浅析Vue3中的响应式原理

29次阅读

共计 10655 个字符,预计需要花费 27 分钟才能阅读完成。

浅析 Vue3 中的响应式原理

请在此前阅读 Vue Composition API 内容,熟悉一下 api。

实现响应式的方式

  1. defineProperty
  2. Proxy

众所周知在 Vue3 中使用了 ES6 提供的ProxyAPI 取代了之前 defineProperty 来实现对数据的侦测.

为啥使用 Proxy?

我们知道在 Vue2 对于监测数组的变化是通过重写了数组的原型来完成的,这么做的原因是:

  1. 不会对数组每个元素都监听,提升了性能.(arr[index] = newValue是不会触发试图更新的, 这点不是因为 defineProperty 的局限性,而是出于性能考量的)
  2. defineProperty 不能检测到数组长度的变化,准确的说是通过改变 length 而增加的长度不能监测到 (arr.length = newLength 也不会)。

同样对于对象,由于 defineProperty 的局限性,Vue2 是不能检测对象属性的添加或删除的。

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {console.log(`get key: ${key} val: ${val}`)
            return val
        },
        set(newVal) {console.log(`set key: ${key} val: ${newVal}`)
            val = newVal
        }
    })
}
function observe(data) {Object.keys(data).forEach((key)=> {defineReactive(data, key, data[key])
    })
}
let test = [1, 2, 3]
observe(test)
console.log(test[1])   // get key: 1 val: 2
test[1] = 2       // set key: 1 val: 2
test.length = 1   // 操作是完成的, 但是没有触发 set
console.log(test.length) // 输出 1,但是也没有触发 get

实现响应式前的一些小细节

相对于 defineProperty,Proxy 无疑更加强大,可以代理数组,并且提供了多种属性访问的方法 traps(get,set,has,deleteProperty 等等)。

let data = [1,2,3]
let p = new Proxy(data, {get(target, key, receiver) {
        // target 目标对象,这里即 data
        console.log('get value:', key)
        return target[key]
    },
    set(target, key, value, receiver) {
        // receiver 最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是 proxy 本身)。// 比如,假设有一段代码执行 obj.name = "jen",obj 不是一个 proxy 且自身不含 name 属性,但它的原型链上有一个 proxy,那么那个 proxy 的 set 拦截函数会被调用,此时 obj 会作为 receiver 参数传进来。console.log('set value:', key, value)
        target[key] = value
        return true // 在严格模式下,若 set 方法返回 false,则会抛出一个 TypeError 异常。}
})
p.length = 4   // set value: length 4
console.log(data)   // [1, 2, 3, empty]

但是对于数组的一次操作可能会触发多次 get/set, 主要原因自然是改变数组的内部 key 的数量了(即对数组进行插入删除之类的操作), 导致的连锁反应。

// data : [1,2,3]
p.push(1)
// get value: push
// get value: length
// set value: 3 1
// set value: length 4
// data : [1,2,3]
p.shift()
// get value: shift
// get value: length
// get value: 0
// get value: 1
// set value: 0 2
// get value: 2
// set value: 1 3
// set value: length 2

同时 Proxy 是仅代理一层的,对于深层对象,也是需要开发者自行实现的,此外对于对象的添加是可以 set traps 侦测到的,删除则需要使用 deleteProperty traps。

let data = {a:1,b:{c:'c'},d:[1,2,3]}
let p = new Proxy(data, {.... 同上})
console.log(p.a)
console.log(p.b.c)
console.log(p.d)
console.log(p.d[0])
p.e = 'e'  // 这里可以看到给对象添加一个属性也依然可以侦测到变化
// get value: a
// 1
// get value: b
// c
// get value: d
// [1, 2, 3]
// get value: d
// 1
// set value: e e

还有一件事,对于一些简单的 get/set 操作,我们在 traps 中使用 target[key]target[key] = value 是可以达到我们的需求的,但是对于一些复杂的如 delet,in 这类操作这样实现就不够优雅了,而 ES6 提供了 ReflectAPI, 且与 Proxy 的 traps 一一对应, 用来代替 Object 的默认行为。
所以我们先将之前的代码改造一下:

let p = new Proxy(data, {get(target, key, receiver) {console.log('get value:', key)
        const res = Reflect.get(target, key, receiver)
        return res
    },
    set(target, key, value, receiver) {console.log('set value:', key, value)
        // 如果赋值成功,则返回 true
        const res = Reflect.set(target, key, value, receiver)
        return res
    }
})

实现响应式

明确目标

我们最终实现的效果应该如下一样:

const a = reactive({b: 0})
effect(() => {console.log(a.b)
})  //effect 在传入时就会自动执行一次
a.b ++ // 这时会打印 1

实现思路

熟悉 Vue2 的同学都知道 Vue2 的响应式是在 get 中收集依赖, 在 set 中触发依赖,Vue3 想必也不例外, 按照这个思路我们的实现步骤如下:

  1. 在触发 get 时收集 effect 函数传入的回调,这里我们称这个回调为 ReactiveEffect
  2. 在 set、deleteProperty… 时触发所有的 ReactiveEffect

下面我们看下具体的实现步骤

reactive 的简单实现

第一步,我们先来简单实现一个可以对对象增删改查侦测的函数
在 set 的实现中,我们将对象的 set 分为两类:新增 key 和更改 key 的 value。通过 hasOwnProperty 判断这个对象是否含有这个属性,不存在存在则是添加属性,存在则判断新 value 和旧 value 是否相同,不同才需要触发 log 执行。
这里的 reactive 函数我们记为 V1 版本。

const hasOwn = (val,key)=>{const res = Object.prototype.hasOwnProperty.call(val, key)
    console.log(val,key,res)
    return res
} 

function reactive(data){
    return new Proxy(data, {get(target, key, receiver) {console.log('get value:', key)
            const res = Reflect.get(target, key, receiver)
            return res
        },
        set(target, key, value, receiver) {const hadKey = hasOwn(target, key)
            const oldValue = target[key]
            const res = Reflect.set(target, key, value, receiver)
            if (!hadKey) {console.log('set value:ADD', key, value)
            } else if (value !== oldValue) {console.log('set value:SET', key, value)
            } 
            return res
        },
        deleteProperty(target, key){const hadKey = hasOwn(target, key)
            const oldValue = target[key]
            const res = Reflect.deleteProperty(target, key)
            if (hadKey) {console.log('set value:DELETE', key)
            }
            return res
        }
    })
}

依赖收集

在 Vue3 中针对所有的被监听的对象,存在一张关系表 targetMap,key 为 target,value 为另一张关系表 depsMap。
depsMap 的 key 为 target 的每个 key,value 为由 effect 函数传入的参数的 Set 集。

type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<string | symbol, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
// 大概结构如下所示
//    target | depsMap
//      obj  |   key  |  Dep 
//               k1   |  effect1,effect2...
//               k2   |  effect3,effect4...
//      obj2 |   key  |  Dep 
//               k1   |  effect1,effect2...
//               k2   |  effect3,effect4...
//

同时我们还需要收集 effect 函数的回调 ReactiveEffect, 当 ReactiveEffect 内有已被监听的对象 get 触发 get 时,便需要一个存储 ReactiveEffect 的地方。这里使用一个数组记录:

const activeReactiveEffectStack: ReactiveEffect[] = []

effect 函数实现如下:

function run(effect,fn,args){
    try {activeReactiveEffectStack.push(effect)
        return fn(...args)   // 执行 fn 以收集依赖
    } finally {activeReactiveEffectStack.pop()
    }
}
function effect(fn,lazy=false){const effect1 = function (...args){return run(effect1, fn, args)
    }
    if (!lazy){effect1()
    }
    return effect1
}

track 跟踪器,由于 get 时的依赖收集:

function track(target,type,key){const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
    if (effect) {let depsMap = targetMap.get(target)
        if (depsMap === void 0) {targetMap.set(target, (depsMap = new Map()))
        }
        let dep = depsMap.get(key)
        if (dep === void 0) {depsMap.set(key, (dep = new Set()))
        }
        if (!dep.has(effect)) {dep.add(effect)
        }
    }
}

trigger 触发器,key 变化 (set,delete…) 时触发, 获取这个 key 所对应的所有的 ReactiveEffect, 然后执行:

function trigger(target,type,key){console.log(`set value:${type}`, key)
    const depsMap = targetMap.get(target)
    if (depsMap === void 0) {return}
    // 获取已存在的 Dep Set 执行
    const dep = depsMap.get(key)
    if (dep !== void 0) {
        dep.forEach(effect => {effect()
        })
    }
}

reactive 函数如下:

function reactive(target){
   const observed = new Proxy(target, {get(target, key, receiver) {const res = Reflect.get(target, key, receiver)
           track(target,"GET",key)
           return res
       },
       set(target, key, value, receiver) {const hadKey = hasOwn(target, key)
           const oldValue = target[key]
           const res = Reflect.set(target, key, value, receiver)
           if (!hadKey) {trigger(target,"ADD",key)
           } else if (value !== oldValue) {trigger(target,"SET",key)
           } 
           return res
       },
       deleteProperty(target, key){const hadKey = hasOwn(target, key)
           const oldValue = target[key]
           const res = Reflect.deleteProperty(target, key)
           if (hadKey) {console.log('set value:DELETE', key)
           }
           return res
       }
   })
   if (!targetMap.has(target)) {targetMap.set(target, new Map())
   }
   return observed
}

深层监听

通常我们都会想到通过递归的方式, 对每个 key 判读啊是否为对象来进行监听,在 Vue3 中:

function get(target, key, receiver) {const res = Reflect.get(target, key, receiver)
    track(target, "GET", key)
    return isObject(res) ? reactive(res) : res
}

Vue3 中,这里做了性能的优化,做了一层 lazy access 的操作,这样只有在访问到深层的对象时才会去做代理。

注意

此时我们所有的 ReactiveEffect 都是和 key 绑定的, 也就是说, 在 ReactiveEffect 函数中, 我们必须 get 一次确定的某个 key, 否则在 set 时是没有 ReactiveEffect 可以触发的, 举个列子:

let data = {a:1,b:{c:'c'}}
let p = reactive(data)
effect(()=>{console.log(p)})
p.a = 3 

上面这种情况是不会打印 p 的.

let data = {a:1,b:{c:'c'}}
let p = reactive(data)
effect(()=>{console.log(p.a)})
p.a = 3  // 3

这种情况才会执行()=>{console.log(p.a), 打印 3

数组类型的问题

但对于数组进行一些操作时,执行起来会有一点小不同, 我们来使用 V1 版本的 reactive 函数来监听一个数组p = reactive([1,2,3]), 并分别对 p 进行操作看看结果:

p.push(1)
// set value:ADD 3 1
p.unshift(1)
// set value:ADD 3 3
// set value:SET 2 2
// set value:SET 1 1
p.splice(0,0,2)
// set value:ADD 3 3
// set value:SET 2 2
// set value:SET 1 1
// set value:SET 0 2
p[3] = 4
// set value:ADD 3 4
--------
p,pop()
// set value:DELETE 2
// set value:SET length 2
p.shift()
// set value:SET 0 2
// set value:SET 1 3
// set value:DELETE 2
// set value:SET length 2
delete p[0]
// set value:DELETE 0
// 这里 p 的 length 依然是三

可以发现当我们对数组添加元素时,对于 length 的 SET 并不会触发(), 而删除元素时才会触发 length 的 SET, 同时对数组的一次操作触发了多次 log。

这里在我们对数组添加操作时就会出现一个问题,我们使用p.push(1), 操作的 index 是 3,上面的列子我们知道在 effect 函数中我们必须 get 这个 3,才会把 ReactiveEffect 给绑定上去, 但那时候是很没有 3 这个 index 的, 所以就会导致没有办法执行 ReactiveEffect。
Vue3 中的处理是在 trigger 中添加一段代码:

function trigger(target,type,key){console.log(`set value:${type}`, key)
    const depsMap = targetMap.get(target)
    if (depsMap === void 0) {return}
    const effects = new Set()
    if (key !== void 0) {const depSet = depsMap.get(key)
        if (depSet !== void 0) {
            depSet.forEach(effect => {effects.add(effect)
            })
        }
    }
    // 就是这里啦,(这里做了一些更改)
    if (type === "ADD" || type === "DELETE") {if(Array.isArray(target)){
            const iterationKey = 'length'
            const depSet = depsMap.get(iterationKey)
            if (depSet !== void 0) {
                depSet.forEach(effect => {effects.add(effect)
                })
            }
        }
    }
    // 获取已存在的 Dep Set 执行
    effects.forEach(effect=>effect())
}

当监听的 target 为数组时, 操作为 ADD 或者 DELETE 时, 触发的 ReactiveEffect 为绑在数组 length 上的, 看下面一段代码:

let data = {foo: 'foo', ary: [1, 2, 3] }
let r = reactive(data)
effect(()=>console.log(r.ary.length))
r.ary.unshift(1)  // 4

验证

我们来拿 Vue3 的代码执行一下看一下是否和我们的一样;
yarn build 之后引入 reactivity.global.js

const {reactive, effect} = VueObserver
let data = {foo: 'foo', ary: [1, 2, 3] }
let r = reactive(data)
effect(()=>console.log(r.ary.length))
r.ary.unshift(1)  // 4
--------------
const {reactive, effect} = VueObserver
let data = {foo: 'foo', ary: [1, 2, 3] }
let r = reactive(data)
effect(()=>console.log(r.ary))
r.ary.unshift(1)  // 没有打印
-------
const {reactive, effect} = VueObserver
let data = {foo: 'foo', ary: [1, 2, 3] }
let r = reactive(data)
effect(()=>console.log(r))
r.foo = 1   // 啥也没打印
--------
const {reactive, effect} = VueObserver
let data = {foo: 'foo', ary: [1, 2, 3] }
let r = reactive(data)
effect(()=>console.log(r.foo))
r.foo = 1   // 1
------
const {reactive,effect} = VueObserver
let data = {foo: 'foo', ary: [1, 2, 3] }
let r = reactive(data)
effect(()=>console.log(r.ary.join()))
r.ary.unshift(1)
// 1,2,3
// 1,2,3,3
// 1,2,2,3
// 1,1,2,3
// 多次打印,证明多次触发

可以发现我们的代码和 Vue3 表现是一致的。
测试的代码都在这个里面➡️代码

总结

到此我们应该算是对 Vue3 中的响应式有一个了解了, 第一次写文章哈,如有错误的地方还望雅正

完整的代码

const activeReactiveEffectStack = []
const targetMap = new WeakMap()
const isObject = (val) => val !== null && typeof val === 'object'
const hasOwn = (val,key)=>{const res = Object.prototype.hasOwnProperty.call(val, key)
    //console.log(val,key,res)
    return res
}
function run(effect,fn,args){
    try {activeReactiveEffectStack.push(effect)
        return fn(...args)   // 执行 fn 以收集依赖
    } finally {activeReactiveEffectStack.pop()
    }
}
function effect(fn,lazy=false){const effect1 = function (...args){return run(effect1, fn, args)
    }
    if (!lazy){effect1()
    }
    return effect1
}
function track(target,type,key){const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
    if (effect) {let depsMap = targetMap.get(target)
        if (depsMap === void 0) {targetMap.set(target, (depsMap = new Map()))
        }
        let dep = depsMap.get(key)
        if (dep === void 0) {depsMap.set(key, (dep = new Set()))
        }
        if (!dep.has(effect)) {console.log(key,effect)
            dep.add(effect)
        }
    }
}
function trigger(target,type,key){console.log(`set value:${type}`, key)
    const depsMap = targetMap.get(target)
    if (depsMap === void 0) {return}
    const effects = new Set()
    if (key !== void 0) {const depSet = depsMap.get(key)
        if (depSet !== void 0) {
            depSet.forEach(effect => {effects.add(effect)
            })
        }
    }
    if (type === "ADD" || type === "DELETE") {if(Array.isArray(target)){
            const iterationKey = 'length'
            const depSet = depsMap.get(iterationKey)
            if (depSet !== void 0) {
                depSet.forEach(effect => {effects.add(effect)
                })
            }
        }
    }
    // 获取已存在的 Dep Set 执行
    effects.forEach(effect=>effect())
}
function reactive(target){
    const observed = new Proxy(target, {get(target, key, receiver) {const res = Reflect.get(target, key, receiver)
            track(target,"GET",key)
            return isObject(res) ? reactive(res): res
        },
        set(target, key, value, receiver) {const hadKey = hasOwn(target, key)
            const oldValue = target[key]
            const res = Reflect.set(target, key, value, receiver)
            if (!hadKey) {trigger(target,"ADD",key)
            } else if (value !== oldValue) {trigger(target,"SET",key)
            }
            return res
        },
        deleteProperty(target, key){const hadKey = hasOwn(target, key)
            const oldValue = target[key]
            const res = Reflect.deleteProperty(target, key)
            if (hadKey) {console.log('set value:DELETE', key)
            }
            return res
        }
    })
    if (!targetMap.has(target)) {targetMap.set(target, new Map())
    }
    return observed
}

正文完
 0