一,前言
上篇,主要介绍了数组依赖收集的原理
- 数组的响应式实现
- 数组的依赖收集方案介绍
- 数组依赖收集的入口
- 添加 dep 后,对象的收益
本篇,数组依赖收集的实现
二,对象依赖收集的总结
{}.dep => watcher
目前,“对象本身”和“对象中的每一个属性”都拥有一个dep属性,用于进行依赖收集,
此时,为对象新增一个不存在的新属性,可以通过对象本身的dep通知到对应的watcher进行视图更新操作;
- 之前:对象本身没有
dep,只有修改对象中已存在的属性,才会触发视图更新; - 现在:对象本身就有
dep,新增对象属性时,可以通知对象本身dep收集的watcher来触发视图更新;
三,数组依赖收集的位置
对象或数组类型会通过 new Observer 创建 observer 实例,
所以,Observer 中的 value 可能是数组,也可能是对象;
Observer 类中的 value,即 this 指 observer 实例,
为其添加 `__ob__` 属性,这样每个对象本身或数组就拥有了 __ob__ 属性;
因此,可以在此处为 observer 实例添加 dep 属性,
这样,相当于为数组或对象本身都增加了一个 dep 属性;
这样一来,无论对象或数组,都可以通过`value.__ob__.dep`获取到`dep`,
当数组数据变化时,就可以通过`dep`中收集的`watcher`来触发视图更新操作;
// todo:这里与上一篇中“4,数组依赖收集的入口”的描述高度相似,可合并;
四,数组和对象本身做依赖收集
在使用defineReactive定义属性时,value值有可能是数组,
对数组的取值操作,会进入Object.defineProperty的get方法,
而在get方法中,会为对象属性、对象和数组本身进行一次依赖收集操作;
// src/observe/index.js
/**
* 给对象Obj,定义属性key,值为value
* 使用Object.defineProperty重新定义data对象中的属性
* 由于Object.defineProperty性能低,所以vue2的性能瓶颈也在这里
* @param {*} obj 需要定义属性的对象
* @param {*} key 给对象定义的属性名
* @param {*} value 给对象定义的属性值
*/
function defineReactive(obj, key, value) {
// childOb 是数据组进行观测后返回的结果(内部 new Observe 只处理数组或对象类型)
let childOb = observe(value);
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if(Dep.target){
// 对象属性的依赖收集
dep.depend();
数组或对象本身也依赖收集
if(childOb){ // 若 childOb 有值,则说明是数组或对象
// 在 observe 方法中,会通过 new Observe 为数组或对象本身添加 dep 属性
childOb.dep.depend(); // 使数组和对象本身的 dep 记住当前 watcher
}
}
return value;
},
set(newValue) {
if (newValue === value) return
observe(newValue);
value = newValue;
dep.notify(); // 对象属性的更新
}
})
}
默认情况下,会为对象或数组本身添加一个dep属性({}.__ob__.dep、[].__ob__.dep),用于依赖收集;
- 当进行数据观测时,会拿到数组的
observer实例(在observer实例中包含__ob__),即返回值childOb;(在defineReactive数据观测方法中,调用observe方法返回new Observer实例,在observer实例中存在dep属性,即childOb.dep) - 在页面对数组进行取值时,如
{{arr}},就一定会进入get方法, - 如果
childOb有值,就让当前数组将依赖收集起来childOb.dep.depend()
这样,就完成了数组的依赖收集
功能测试:
为数组本身添加了dep属性,收集渲染watcher;
五,数组中嵌套对象(对象或数组)的递归处理
在数组中,还有可能会继续嵌套着数组或对象,比如:[{}]或[[]]或是[[[[]]]];
目前代码版本:
- 只会对数组外层进行依赖收集;
- 不会对数组内部嵌套的数组进行依赖收集;
注意:此时,数组中嵌套的对象是能够进行依赖收集的;
1,数组中嵌套对象的依赖收集原理
例如:arr:[{a:1},{b:2}]
- 当对
arr取值时{{arr}},默认会对arr进行JSON.stringify(arr), JSON.stringify操作,将会取出数组内部所有属性进行打印输出,- 相当于
JSON.stringify会对内部属性进行取值操作,即会进入getter方法, - 而在
getter方法中,就会为对象本身和对象内部的属性进行依赖收集;
所以,在这种情况下,默认就会进行依赖收集操作;
<body>
<div id=app>
{{arr}}
</div>
<script>
// 测试数组的依赖收集
let vm = new Vue({
el: '#app',
data() {
return { arr: [{ a: 1 }, { b: 2 }] }
}
});
// 更新数组中对象的属性值
vm.arr[0].a = 100;
console.log("输出当前 vm", vm);
</script>
</body>
页面输出:[{"a":100},{"b":2}]
分析说明:
- 对数组
arr的取值操作时,内部会对arr做JSON.stringify操作,会对对象中所有属性进行取值,这里就会做一次依赖收集; - 所以,更新数组中对象的属性值
a,实际执行的是对象的更新操作,与外层的数组无关;
通过控制台观察,当前数组中对象的属性是有dep的:
2,数组中嵌套数组的依赖收集实现
例如:arr:[[1][2]]
- 当前,对数组
arr取值时{{arr}},仅对外层数组本身进行了依赖收集,数组内部的数组并没有进行依赖收集; - 所以,当执行
arr[0].push()操作时,会直接操作内部的数组,就不会触发视图的更新了;
当数组中存在数组时(如:[[]]),需要对所有数组进行依赖收集:
- 所以,需要对数组递归做依赖收集,循环数组,让数组中的每一个属性都进行依赖收集;
当数组中存在对象时(如:[{}]),未来可能为对象新增属性:
- 所以,数组中的对象也需要做依赖收集,为对象本身做依赖收集才能触发视图更新;
综上,不论是对象还是数组,只要外层数组的里面是对象,就将里面的对象或数组都进行依赖收集;
// src/observe/index.js
function defineReactive(obj, key, value) {
let childOb = observe(value);
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if(Dep.target){
dep.depend();
if(childOb){
childOb.dep.depend();
if(Array.isArray(value)){// 如果当前数据是数组类型
dependArray(value) // 数组中可能嵌套数组或对象,需要递归处理
}
}
}
return value;
},
set(newValue) {
if (newValue === value) return
observe(newValue);
value = newValue;
dep.notify();
}
})
}
/**
* 使数组中的引用类型(数组、对象)都进行依赖收集
* @param {*} value 需要做递归依赖收集的数组
*/
function dependArray(value) {
// 在数组中如果存在对象:[{}]或[[]],需要做进行依赖收集(后续可能会为对象新增属性)
for(let i = 0; i < value.length; i++){
let current = value[i];
// 在每一项 current 上,如果有__ob__,则说明是对象(只有对象上才有 __ob__),使用 dep 收集依赖
current.__ob__ && current.__ob__.dep.depend();
// 如果数组内部的数组的内部还是数组,比如:[[[]]],需要继续递归处理
if(Array.isArray(current)){
dependArray(current)
}
}
}
注意:虽然,之前已经对数组进行了递归观测,但用户使用数据时并不是递归使用的;
- 数据观测时是对
arr:[[[[[]]]]]- 用户使用是:{{arr}} 只取了最外层
- 最终显示在页面上是所有的内容 所以,更新数据时修改内部数据依然需要更新;
注意:
arr:[{a:1},{b:2}]这种情况下,{{arr}} 取值会执行JSON.stringify(arr)当数据更新时arr[0].a = 100可以会触发更新
- 因为
JSON.stringify(arr)对对象的属性进行了取值操作,在取值时就对对象中的属性进行了依赖收集,- 但是,外层的对象本身是不会做依赖收集的, 所以,在这里弥补了这个漏洞,通过
dependArray,不论数组内部是对象或数组都会对其本身进行一次依赖收集,即数组中的所有引用类型都进行了收集依赖;
功能测试:
<body>
<div id=app> {{arr}} </div>
<script>
let vm = new Vue({
el: '#app',
data() {
return { arr: [[]] }
}
});
console.log("输出当前 vm", vm);
</script>
</body>
页面输出:[[]]
外层数组本身和内层数组都被添加了dep,收集渲染watcher:
3,数组的视图更新
上边,已经完成了数组的依赖收集
但是,目前执行arr.push()操作还不能更新视图,因为此时还没有调用更新方法
所以,当执行arr.push等操作改变原数组时,还要再次触发数组的依赖更新,即通过ob拿到dep并调用notify;
// src/observe/array.js
let oldArrayPrototype = Array.prototype;
export let arrayMethods = Object.create(oldArrayPrototype);
let methods = [
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
]
methods.forEach(method => {
arrayMethods[method] = function (...args) {
oldArrayPrototype[method].call(this, ...args)
let inserted = null;
let ob = this.__ob__; // 获取数组上的 __ob__
switch (method) {
case 'splice':
inserted = args.slice(2);
case 'push':
case 'unshift':
inserted = args;
break;
}
if(inserted)ob.observeArray(inserted);
// 通过 ob 拿到 dep,调用 notify 触发 watcher 执行视图更新
ob.dep.notify();
}
});
功能测试:
<body>
<div id=app> {{arr}} </div>
<script>
let vm = new Vue({
el: '#app',
data() {
return { arr: [[]] }
}
});
vm.arr[0].push(100); // 修改数组中的数组
</script>
</body>
// 页面输出:[[100]]
修改数组内部嵌套的数组,能够触发视图的更新操作;
六,总结
响应式数据原理,分为对象和数组两大类,在Vue的初始化过程中:
- 1,会对对象的每个属性进行劫持,从而为对象中的所有属性添加一个
dep属性(取值时做依赖收集); - 2,还会对属性值为对象或数组的本身增加
dep属性,进行依赖收集; - 3,如果是属性更新,将触发属性对应的
dep执行更新操作; - 4,如果是数组更新,将触发数组本身的
dep执行更新操作; - 5,如果取值时是数组,还要让数组中的对象类型(数组中嵌套的对象或数组)也进行依赖收集(递归依赖收集);
- 6,如果数组中嵌套了对象,由于对象取值会进行
JSON.stringify操作,所以,对象中的属性默认就会做依赖收集;
七,结尾
本篇,主要介绍了数组依赖收集的实现
- 对象依赖收集的总结;
- 数组依赖收集的位置;
- 数组和对象本身做依赖收集;
- 数组中嵌套对象(对象或数组)的递归处理;
下一篇,Vue 生命周期的实现
更新日志
- 20210629:
- 添加 5-3、数组的视图更新部分
- 添加各种情况的测试 Demo、截图、部分文案调整
- 添加 6、总结部分
- 20210805:
- 更新“结尾”部分与文章摘要
- 20230208:
- 添加部分代码注释,优化部分内容说明,添加内容中的代码高亮;
- 20230210:
- 修改部分可能存在歧义和理解不清晰的内容描述;