Vue3响应系统的作用与实现

 副作用函数的执行会直接或间接影响其他函数的执行。一个副作用函数中读取了某个对象的属性,当该属性的值发生改变后,副作用函数自动重新执行,这个对象就是响应式数据。

1 响应式系统的实现

拦截对象的读取和设置操作。当读取某个属性值时,把副作用函数存储到一个“桶”里,而设置该属性值时,则将这个副作用函数从“桶”中取出病执行。

/**
 * 响应式系统基本原理:Proxy 拦截设置及读取操作,读取属性时将副作用
 * 函数存于桶,设置属性时将副作用函数从桶中取出并执行
 */
let obj = {name: '',tag: false,count: 0,num1: 0,num2: 0}

let bucket = new Set()

let proxyObj = new Proxy(obj,{

    get(target, p, receiver) {
        bucket.add(fun)
        return target[p]
    },

    set(target, p, newValue, receiver) {
        target[p] = newValue
        bucket.forEach(fn => fn())
    }
})

function fun() {
    console.log(proxyObj.name)
}

fun() // 触发执行,空字符串
proxyObj.name = "hello" // hello
proxyObj.name = "js" // js

1.1 桶的结构

存储副作用函数的“桶”,应该为不同的对象、及其属性存储对应的副函数集。存储的容器为WeakMap。

/**
 * 用WeakMap 作为副作用函数的容器,改进响应式系统,支持不同的
 * 响应式对象及其属性都能响应式执行
 */
let obj = {name: '',tag: false,count: 0,num1: 0,num2: 0}

let bucketMap = new WeakMap()
let activeFun // 用于指示当前需要注册的副作用函数

let proxyObj = new Proxy(obj,{

    get(target, p, receiver) {
        track(target,p)
        return target[p]
    },

    set(target, p, newValue, receiver) {
        target[p] = newValue
        trigger(target,p)
    }
})

function track(target,p) { // 跟踪函数
   if (activeFun) {
       let map = bucketMap[target]
       if (!map) map = bucketMap[target] = new Map()
       let set = map[p]
       if (!set) set = map[p] = new Set()
       set.add(activeFun)
   }
}

function trigger(target,p) { // 触发函数
    let map = bucketMap[target]
    if (map) {
        let set = map[p]
        set && set.forEach(fn => fn())
    }
}

function effect(fn) { // 用于注册副作用函数
    let tempFun = () => {
        activeFun = fn
        fn()
        activeFun = null
    }
    tempFun()
}

effect(() => {
    console.log(proxyObj.name,proxyObj.tag)
})
effect(() => {
    console.log("name2",proxyObj.name)
})
console.log("------------------------------------")
proxyObj.name = "hello"
console.log("------------")
proxyObj.tag = false
console.log("------------")
proxyObj.name = "js";
console.log("------------")

1.2 分支切换

分支切换是指,函数内部存在一个三元表达式,根据某个字段的值会执行不同的代码分支。当该字段的值发生变化时,代码执行的分支会跟着变化。

例如:console.log(proxyObj.tag ? proxyObj.name : "false");

按照上面的代码,当name或tag的值被设置时,都会触发副作用函数。但是,在副作用函数中,当tag为false时,name的值是不会被显示的,这意味着,当tag为false时,无论name被设置多少次,都不希望执行这个副作用函数。

解决方案:当该副作用函数被触发时,删除属性与该函数的关系。在副作用函数执行时再重新创建关系。

function track(target,p) { // 跟踪函数
   if (activeFun) {
       let map = bucketMap[target]
       if (!map) map = bucketMap[target] = new Map()
       let set = map[p]
       if (!set) set = map[p] = new Set()
       set.add(activeFun)
       activeFun.funSetList.push(set) 
   }
}

function effect(fn) { // 用于注册副作用函数
    let tempFun = () => {
        cleanup(tempFun)
        activeFun = tempFun
        fn()
        activeFun = null
    }
    tempFun.funSetList = []
    tempFun()
}

function cleanup(fn) {
    fn.funSetList.forEach(set => {
        set.delete(fn)
    })
    fn.funSetList = []
}

1.3 嵌套的effect

组件在渲染时,会执行effect函数来注册副作用函数,而父组件在渲染时,不仅会执行其本身的effect函数,还会自行其子组件的effect,这是就发生了嵌套的effect的调用。即如下:

effect(() => {
    effect(() => {
        console.log(proxyObj.count)
    })
    console.log(proxyObj.tag);
})

当修改tga 属性时,父组件的副作用函数并不会执行。

解决方案:创建一个注册的副作用函数指示栈。副作用函数执行前,将函数压入到栈中,执行完后则弹出该函数。

let activeFunStack = []
let registerFunSet = new Set() // 防止函数多次被注册

function effect(fn) { // 用于注册副作用函数
    if (!registerFunSet.has(fn)) {
        registerFunSet.add(fn)
        let tempFun = () => {
            cleanup(tempFun)
            activeFun = tempFun
            activeFunStack.push(activeFun)
            fn()
            activeFunStack.pop()
            activeFun = activeFunStack.length < 1 ? null : activeFunStack[activeFunStack.length - 1]
        }
        tempFun.funSetList = []
        tempFun()
    }
}

effect(() => {
    effect(sonFun)
    console.log(proxyObj.tag);
})

function sonFun() {
    console.log(proxyObj.count)
}

console.log("------------------------------------")
proxyObj.tag = false
proxyObj.count = 1

1.4 避免无限递归

在一个副作用函数设置及读取同一个属性时,上面代码中,会发生无限递归对情况。这是因为,当设置属性值时,会触发副作用函数执行,而副作用函数中又会设置该属性值…

解决方案:在触发时,不执行当前正在被注册的副作用函数。

function trigger(target,p) { // 触发函数
    let map = bucketMap[target]
    if (map) {
        let set = map[p]
        if (set) {
            let tempSet = new Set(set)
            tempSet.forEach(fn => {
                if (activeFun !== fn) fn()
            })
        }

    }
}

effect(() => {
    console.log(proxyObj.count++);
})
console.log("------------------------------------")
proxyObj.count++;

1.5 调度执行

可调度,是指当动作触发副作用函数重复执行时,有能力决定副作用函数执行的时机、次数以及方式。

1.5.1 微任务

宏任务

通常是由宿主环境(浏览器)提供的。包括但不限于:script(整体代码)、setTimeout、setInterval、I/O、UI渲染。

微任务

由JS引擎(如V8)提供的。它们在当前宏任务之后,下一个宏任务之前执行。常见的微任务:Promose.then()

微任务通常用于执行需要尽快完成的异步操作。

过多使用微任务可能会导致主线程被阻塞,影响页面的响应。

表 宏任务和微任务两种类型的队列

执行顺序:

  1. 宏任务队列:从宏任务队列中取出一个任务执行。
  2. 执行宏任务:执行宏任务中的所有同步代码。
  3. 微任务队列:在执行完宏任务中的所有同步代码后,会查看并清空微任务队列中的所有任务。
  4. 渲染UI:微任务队列清空后,浏览器会进行UI渲染(如果需要)。
  5. 循环:重复步骤1~4,直到宏任务队列和微任务队列都为空。
function trigger(target,p) { // 触发函数
    let map = bucketMap[target]
    if (map) {
        let set = map[p]
        if (set) {
            let tempSet = new Set(set)
            tempSet.forEach(fn => {
                if (activeFun !== fn) {
                    if (fn.options.scheduler) {
                        fn.options.scheduler(fn)
                    } else {
                        fn()
                    }
                }
            })
        }
    }
}

function effect(fn,options = {}) { // 用于注册副作用函数
    if (!registerFunSet.has(fn)) {
        registerFunSet.add(fn)
        let tempFun = () => {
            cleanup(tempFun)
            activeFun = tempFun
            activeFunStack.push(activeFun)
            fn()
            activeFunStack.pop()
            activeFun = activeFunStack.length < 1 ? null : activeFunStack[activeFunStack.length - 1]
        }
        tempFun.options = options
        tempFun.funSetList = []
        tempFun()
    }
}

const jobQueue = new Set()
const promise = Promise.resolve()
let isFlushing = false

function flushJob() {
    if (!isFlushing) {
        isFlushing = true
        promise.then(() => {
            jobQueue.forEach(fn => fn())
        }).finally(() => {
            isFlushing = false
        })
    }
}

effect(() => {
    console.log(proxyObj.count);
},{
    scheduler(fn) {
        jobQueue.add(fn)
        flushJob()
    }
})
console.log("------------------------------------")
proxyObj.count++;
proxyObj.count++;

1.6 计算属性computed 与 lazy

计算属性,只有当相关依赖发生改变时,计算属性才会重新求值。否则,就是多次访问计算属性,也会立即返回之前的计算结果,不需要再次执行函数。

function effect(fn,options = {}) { // 用于注册副作用函数
    if (!registerFunSet.has(fn)) {
        registerFunSet.add(fn)
        let tempFun = () => {
            cleanup(tempFun)
            activeFun = tempFun
            activeFunStack.push(activeFun)
            let res = fn()
            activeFunStack.pop()
            activeFun = activeFunStack.length < 1 ? null : activeFunStack[activeFunStack.length - 1]
            return res
        }
        tempFun.options = options
        tempFun.funSetList = []
        if (!options.lazy) {
            tempFun()
        }
        return tempFun
    }
}

function computed(fn) {
    let value
    let dirty = true
    let tempFun = () => { trigger(obj,"value") }
    let effectFn = effect(fn,{
        lazy: true,
        scheduler() {
            if (!dirty) {
                dirty = true
                jobQueue.add(tempFun)
                flushJob()
            }
        }
    })
    let obj = {
        get value() {
            if (dirty) {
                value = effectFn()
                dirty = false
            }
            track(obj,"value")
            return value
        }
    }
    return obj
}

let computedRes = computed(() => proxyObj.num1 + proxyObj.num2)

effect(()=> {
    console.log("computedRes1",computedRes.value)
})

proxyObj.num1 = 2
proxyObj.num2 = 3

1.7 watch 的实现原理

Vue 的 watch,可以监听对象、对象的某个属性。可以对对象进行深层次监听。当属性值改变时,会触发监听的回调函数。

function watch(source,callBack) {
    let newValue,oldValue
    let getter
    if (typeof source === "function") {
        getter = source
    } else {
        getter = () => traverse(source)
    }
    const job = () => {
        newValue = effectFun()
        callBack(newValue,oldValue)
        if (typeof newValue === "object") {
            oldValue = {...newValue}
        } else {
            oldValue = newValue
        }
    }
    let effectFun = effect(getter,{
        scheduler() {
            job()
        }
    })
    let tempRes = effectFun()
    if (typeof tempRes === "object") {
        oldValue = {...tempRes}
    } else {
        oldValue = tempRes
    }
}

function traverse(value) {
    if (typeof value != "object" || value === null) return
    for (const k in value) traverse(value[k])
    return value
}

watch(proxyObj,(newValue,oldValue) => {
    console.log("proxyObj",newValue,oldValue)
})

watch(proxyObj.name,(newValue,oldValue) => {
    console.log("name",newValue,oldValue)
})

proxyObj.count = 1
proxyObj.name = "hello"

1.8 过期的副作用

竞态问题指的是两个或多个操作几乎同时发生,并且结果依赖于它们发生的顺序,但顺序又是不确定的。 在单线程JS环境中(浏览器),我们通常不会遇到竞态问题,但是,随着Web API的引入(如异步操作,Promises,async/aswait,Web Workers等),导致JS代码中仍然可以出现竞态问题。

watch(() => proxyObj.count,(newValue,oldValue) => {
    Promise.resolve().then(() => {
        setTimeout(() => {
            console.log(newValue)
        },newValue * 1000)
    })
})

proxyObj.count = 5
setTimeout(()=> {
    proxyObj.count = 2
},500)

解决方案:在第二次触发时,将前一次的触发状态设置为过期,只有状态非过期,产生的结果才有效。

function watch(source,callBack) {
    let newValue,oldValue
    let getter
    if (typeof source === "function") {
        getter = source
    } else {
        getter = () => traverse(source)
    }
    let cleanup
    let cleanFun = (fn) => {
        cleanup = fn
    }
    const job = () => {
        if (cleanup) cleanup()
        newValue = effectFun()
        callBack(newValue,oldValue,cleanFun)
        if (typeof newValue === "object") {
            oldValue = {...newValue}
        } else {
            oldValue = newValue
        }
    }
    let effectFun = effect(getter,{
        scheduler() {
            job()
        }
    })
    let tempRes = effectFun()
    if (typeof tempRes === "object") {
        oldValue = {...tempRes}
    } else {
        oldValue = tempRes
    }
}

watch(() => proxyObj.count,(newValue,oldValue,cleanFun) => {
    let expire = false
    cleanFun(() => {
        expire = true
    })
    Promise.resolve().then(() => {
        setTimeout(() => {
            if (!expire) console.log(newValue)
        },newValue * 1000)
    })
})

相关推荐

  1. Vue3响应系统作用实现

    2024-07-11 11:32:02       17 阅读
  2. Vue3实现响应式编程

    2024-07-11 11:32:02       44 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-11 11:32:02       53 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-11 11:32:02       56 阅读
  3. 在Django里面运行非项目文件

    2024-07-11 11:32:02       46 阅读
  4. Python语言-面向对象

    2024-07-11 11:32:02       57 阅读

热门阅读

  1. 数据结构第19节 排序算法(1)

    2024-07-11 11:32:02       17 阅读
  2. HOW - 黑暗模式 Dark Mode

    2024-07-11 11:32:02       20 阅读
  3. Conda:Python环境管理的瑞士军刀

    2024-07-11 11:32:02       20 阅读
  4. linux之常见的coredump原因都有哪些

    2024-07-11 11:32:02       20 阅读
  5. DSOX3024A 示波器200 MHz,4 通道

    2024-07-11 11:32:02       15 阅读
  6. react学习——23react中的路由的使用(重要)

    2024-07-11 11:32:02       18 阅读
  7. Mac OS ssh 连接提示 Permission denied (publickey)

    2024-07-11 11:32:02       20 阅读
  8. C++ 字符串哈希(hush)讲解

    2024-07-11 11:32:02       18 阅读
  9. 玩转springboot之SpringBoot单元测试

    2024-07-11 11:32:02       21 阅读
  10. 使用 Nuxt 3 搭建国际官网

    2024-07-11 11:32:02       17 阅读
  11. kafka-3

    kafka-3

    2024-07-11 11:32:02      16 阅读