Vue响应式细节小结

289 阅读4分钟

跟着读源码的教程写了一下Vue的响应式部分。因为不是完全照着Vue源码来写,教程也会遗漏一些细节,出现了一些细节问题导致的bug。

  • 遍历状态监测 ,__ob__设置问题

__ob__必须使用 Object.definedproperty设置它的是否可以枚举的属性,Vue把它封装成了一个函数

//定义部分
function def(obj: object, key: string, val: any, enumerable: boolean = false) {
    Object.defineProperty(obj, key, {
      value: val,
      enumerable: enumerable,
      writable: true,
      configurable: true
    })
  }
//使用部分,value是被转换为响应式的对象,this是Observer实例
def(value, '__ob__', this, false)

可以看出来为什么使用 Object.defineProperty定义属性而不是直接使用 value.__ob__=this定义吗?

因为**enumerabl被设置成true了**,这意味着它在使用 Object.keys()或其他遍历对象的方法,将不会被枚举到。

如果没有设置它,在递归遍历时,将会出现递归无法终止导致爆栈。

  • Watcher实例重复被添加问题

很多分析,没有提及这个细节,但是我之前一直在思考这个问题,昨天也遇到了。

当转换为响应式的Object被Watcher观测到a属性后,每次访问a属性,就会在get属性被添加进一次subs(依赖数组),导致set时重复响应。

Vue解决的方式很简单,先给Dep实例添加编号,编号是递增的

//Dep构造函数内
construcor(){
    //省略...
    this.id = uid++;
    //省略
}

同时添加的代码片段增加判断内容

//Wathcer构造函数内
construcor(){
    //省略...
    this.depIds = new Set<number>();
    //省略
}
//Watcher实例的方法
addDep(dep:Dep){
        const { id } = dep;
        if(!this.depIds.has(id)){
            dep.addSub(this)
            this.depIds.add(id);
        } 

    }

Set类型的检索会快很多。不过为什么subs数组不是Set类型,应该是出于性能的考量,比较数字远比对象快得多。

  • 数组添加依赖失败问题

测试时发现,数组无法响应。问题根源在于,数组的依赖位置和键依赖位置不同。

const obj = {
	arr:[1,2,3]
}
new Oberserve(obj);
obj.arr.push(4)//arr数组响应
obj.arr = 1; //obj的arr键响应

我们看到obj上的arr键与arr所指向的数组的响应机制是不同的,对应的Dep实例也是不同的。

首先,每个Observer实例上都有一个Dep实例

class Observer {
//
	constructor(){//参数省略
		 this.dep = new Dep()//这个dep是给数组用的
	}
//省略其他...
}

我们看看getter函数,

const dep = new Dep();
const childOb = observe(value);
//getter函数
function reactiveGetter() {
            //添加一个依赖
            dep.depend();
            if (childOb) {
                //对象上的依赖
                childOb.dep.depend();
                if (Array.isArray(value)) {
                    //Array的逻辑是不一样的,需要继续处理
                    dependArray(value)
                }
            }
            return value;
        },

reactiveGetter函数内,dep.depend将收集有关arr键的依赖,而 childOb.dep将收集有关arr键指向数组的依赖。闭包变量dep负责键值,arr.__ob__.dep负责数组。

Dep类与Watcher类的关系

下面是一个Demo的示例

const a = 1{
    a: 1,
    b: {
        c: 2
    }
};
new Oberserve(a);

首先,a已经转换成响应式。

响应式的每个属性Setter内部都引用了一个Dep实例。你可能会奇怪,基础类型不可能挂载属性,Dep实例放在哪里?

它使用了闭包,每个属性自身都对应一个Dep实例;

//defineReactive 定义响应式函数定义内
defineReactive(obj: Own.ExternalObject, key: string, value?: unknown) {
    if (value === undefined) {
        //没传值就是其本身 传值则覆盖
        value = obj[key];
    }
    let childOb: Observer;
    if (isObject(value)) {
        //进入递归
        //递归结束条件 非object类型,即基本类型
        childOb = <Observer>observe(<Own.ExternalObject>value);
    }
    const dep = new Dep();
    Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: true,
        //这里是两个闭包,作用域链上的value变量作为私有属性
        get: function reactiveGetter() {
            //添加一个依赖
            dep.depend();
            return value;
        },
        set: function reactiveSetter(newValue) {
            if (newValue === value) {
                return;
            }
            //通知依赖;
            value = newValue;
            dep.notify();
        }
    })
}

所以已经明确,属性与Dep实例是一对一的关系。

接着,new Watcher()输入a,键或者函数,回调函数。

function log(thisValue:unknown,oldValue:unknown){
    return console.log(`new:${thisValue},old:${oldValue}`);
}
const w1 = new Watcher(a,"b.c",log)
const w2 = new Watcher(a,function(vm){
    return this.a + this.e
},log)
const w3 = new Watcher(a,function(vm){
    return this.a
},function(){})

我们看到,一个Watcher实例(w2)是可以同时观测多个属性(computed的原理),所以,一个Watcher实例是会被多个Dep收集的,同时,一个Watcher实例也会把收集它的Dep实例记录在案,比如 w2 就记录了ae对应的Dep实例。它们互相记录,方便通知和后续操作。

同时,一个属性又会被多个Wathcer实例观测,比如w2w3,a对应的Dep实例中就收集了两个Watcher实例。

所以其实它们是多对多的关系。