vue3的响应式原理和虚拟DOM

2 篇文章 0 订阅

为啥使用Proxy?而弃用defineProperty

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

相对于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的数量了(即对数组进行插入删除之类的操作),导致的连锁反应

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

实现响应式

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

在触发get时收集effect函数传入的回调,这里我们称这个回调为ReactiveEffect
在set、deleteProperty…时触发所有的ReactiveEffect
下面我们看下具体的实现步骤

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

    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 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的地方。这里使用一个数组记录:

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进行操作看看结果:

// 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中添加一段代码:

    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 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
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值