跟着读源码的教程写了一下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
就记录了a
和e
对应的Dep
实例。它们互相记录,方便通知和后续操作。
同时,一个属性又会被多个Wathcer
实例观测,比如w2
和w3
,a对应的Dep
实例中就收集了两个Watcher
实例。
所以其实它们是多对多的关系。