《vue.js的设计与实现》4-6章小结

一、响应系统的整体实现

p49页的代码所示,Vue3使用es6中的proxy进行数据劫持,当副作用函数(用到某个属性的方法)第一次执行的时候,会触发拦截器中的get方法,然后调用了track方法将这个副作用函数添加到存储副作用函数的桶中,相当于收集依赖的过程。
这个存储副作用函数的桶的结构为:用weakmap存储不同对象(weakmap当key没有引用被垃圾回收器回收了,对应的键和值就访问不到了,防止内存溢出),用map存储对象的各个属性,各属性的副作用函数用set来存储(可以去重)。(这样的设计当一个属性值变化时,只会触发与这个属性值相关的副作用函数重新执行)
当属性的值发生变化时,就调用trigger函数找到之前收集到的这个属性对应的所有副作用函数并执行。

二、需要考虑的问题

1、清除遗留的副作用函数问题
如果一个副作用函数中存在这样一个三元表达式:

document.body.innerText = obj.ok?obj.text:'not'

当obj.ok为true的时候,这个副作用函数执行会触发ok和text这两个属性的读取操作,那么这个副作用函数会被分别收集到这两个属性对应的set中。当obj.ok改为false后,改变obj.text就不再应该触发这个副作用函数的重新执行,因此我们需要在track方法中同时收集与该副作用函数相关联的依赖,存在一个数组中,并在每次副作用函数执行时,将该副作用函数从依赖中删除,若之后读取时又执行了该副作用函数,则重新收集到依赖中。
2、副作用函数嵌套的问题
如果我们只用一个全局变量activeeffect来存储副作用函数,那么同时存储的只能有一个,当副作用函数发生嵌套的时候,内层副作用函数的执行会覆盖activeeffect的值。为解决这个问题,需要一个副作用函数栈,在副作用函数执行时压入栈,执行完后弹出,并始终让activeeffect指向栈顶的副作用函数。
3、避免无限递归循环
当副作用函数中执行obj.foo++,这个操作既会读取值,又会设置值,就会无限递归的调用自己,解决方法就是在trigger方法中判断触发执行的副作用函数与当前正在执行的副作用函数是否相同,若相同则不触发执行。
4、调度执行
在vue中连续多次修改响应式数据只会触发一次更新,原理是在副作用函数中允许传入一个调度器,在trigger方法中判断若副作用函数有调度器,就调用该调度器,并把副作用函数作为参数传递。

三、computed和watch

1、computed
添加lazy属性,副作用函数懒执行,只有当读取value值的时候,才会执行副作用函数并返回结果。添加dirty标志,初始为true,当dirty为true的时候就要对值进行重新计算,false就用上一次缓存的值。
2、watch
传入要watch的对象及回调函数,在effect函数中递归的读取要watch的对象,建立响应式数据与副作用函数的联系,当数据发生变化时,会触发调度器中的回调函数进行执行(如果一个副作用函数存在调度器(用户传入),那么在trigger函数中就调用调度器,并将副作用函数作为参数传入)。为了拿到新值和旧值传入回调函数,创建懒执行的effect,然后在effect函数中手动调用副作用函数拿到旧值,然后在调度器中再次执行副作用函数得到新值,这样就可以将新值与旧值传入回调函数作为参数。immediate属性就是在绑定的时候就先执行一次回调函数。deep属性表示如果监听一个对象,是否也监听到对象里面的属性变化。

四、proxy和reflect

reflect是一个全局对象,有很多方法,比如

Reflect.get(target,key,receiver)
//相当于
target.key

第三个参数receiver其实就是用proxy代理的对象p。

const obj = {
    foo:1,
    get bar(){
        return this.foo
    }
}
effect(()=>{
    console.log(p.bar)
})
const p = new Proxy(obj,{
    get(target,key){
        track(target,key)
        //return target[key]
        return Reflect.get(target,key,receiver)
    }
})

当第一次执行副作用函数时,读取p.bar属性,由于p.bar是访问器属性,那么副作用函数与属性foo间应建立响应式关系。但是,如果用target[key]的方法读取,get bar()中的this相当于原对象obj,那就无法建立副作用函数与属性foo间的响应式关系,此时若修改p.foo的值,副作用函数就不会执行。用Reflect.get,此时get bar()中的this就相当于代理对象p,就可以建立起响应式关系。

五、代理Object

js引擎内部有许多方法,当我们在操作一个对象的时候,其实就是引擎在调用内部的方法。如果一些对象内部的方法逻辑与普通对象不同,那就是异质对象。Proxy就是一个异质对象,我们在创建代理对象时传入的get函数,其实就是用来自定义代理对象的内部方法。
对一个普通对象的读取操作有以下几种可能:

obj.foo
key in obj
for(const key in obj)

第一中读取方式就自定义get方法进行拦截,第二种自定义has方法拦截,第三种自定义ownKeys方法拦截。
自定义set拦截函数时,需要考虑是添加新属性还是设置已有属性。
删除属性操作要自定义deleteProperty函数进行拦截。
当触发响应时,要避免对象自身上没有这个属性,就要获取对象原型的属性,这样副作用函数就会执行两次,因此在set拦截函数中就要判断receiver是不是target的代理对象。

六、代理数组

对数组的读取操作包括:
1、通过索引访问arr[0]
普通代理就能响应
2、访问数组长度arr.length
3、把数组作为对象,使用for…in循环遍历
自定义ownkeys拦截
4、使用for…of循环遍历
普通代理就能响应,因为只要数组的长度和元素值发生变化,副作用函数都会重新执行
5、不改变原数组的原型方法如concat/join
对数组的设置操作包括:
1、通过索引修改元素值arr[0] = 3
判断设置的索引值是否大于当前数组长度,若大于则还要触发与length相关的副作用函数执行
2、修改数组长度arr.length = 0
只有索引值大于等于新length属性的元素才需要触发响应
3、数组的栈方法
既会读取数组的length属性,又会设置,因此要屏蔽读取
4、修改原数组的原型方法如splice/sort

七、原始值的响应方案

封装一个ref函数,在函数内部创建包裹对象,然后使用reactive函数将包裹对象变成响应式数据并返回。用object.defineproperty定义一个不可枚举且不可写的属性来区分是普通对象还是包裹对象。
响应丢失问题:

//obj是响应式数据
const obj = reactive({foo:1,bar:2})
//展开后的newobj就是一个普通对象,不具有响应式
const newobj = {
    ...obj
}
effect(()=>{
    console.log(newobj.foo)
})
//不会触发副作用函数执行
obj.foo = 100

如果要在副作用函数内通过普通对象也能建立响应,就要修改这个普通对象的实现方式:

const newobj = {
    foo:toRef(obj,'foo'),
    bar:toRef(obj,'bar')
}
//或
const newobj = {...toRefs(obj)}
function toRef(obj,key){
    const wrapper = {
        get value(){
            return obj[key]
        }
    }
    return wrapper
}
function toRefs(obj){
    const ret = {}
    for(const key in obj){
        ret[key] = toRef(obj,key)
    }
    return ret
}

toRef的作用就是针对一个响应式对象(reactive 封装)的属性创建一个新的包裹对象,并保持响应式关系。
toRefs的作用就是将一个响应式对象的所有属性都创建新的包裹对象,并保持响应式关系。
在setup()函数中return{…obj}会使obj失去响应,因为相当于返回了一个新的普通对象,此时就需要const newobj = {…toRefs(obj)}并return{…newobj}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值