一、介绍
在 Vue 初始化过程中:
- 通过对象属性劫持,会为所有属性添加 dep
- 还会为属性值进行依赖收集:为对象本身和数组也添加 dep
- 如果是属性变化,将触发属性对应的 dep 去做更新;
- 如果是数组更新,将触发数组本身的 dep 去做更新;
- 如果取值时属性值为数组,数组中的对象类型(数组中嵌套的对象或数组)递归进行依赖收集
- 如果数组中嵌套了对象,由于对象取值会进行 JSON.stringify,所以对象中的属性默认就会做依赖收集
二、数组依赖收集的原理
1,数组的响应式实现
在响应式实现中,数组数据类型是通过重写能够改变原数组的 7 个方法实现的
收集数组所依赖的渲染 watcher,当数组更新时,触发对应 watcher 更新即可
2,数组的依赖收集方案
对象的依赖收集方案:为对象的每一个属性都增加一个 dep 属性用于做依赖收集
同理,也可以为数组增加 dep 属性用于收集依赖,
当数组更新时,通知数组依赖做视图更新
3,数组依赖收集的入口
js
class Observer {
constructor(value) {
Object.defineProperty(value, '__ob__', {
value:this,
enumerable:false
});
if (isArray(value)) {
value.__proto__ = arrayMethods;
this.observeArray(value);
} else {
this.walk(value);
}
}
对象或数组类型的数据会通过 new Observer 创建 observer 实例,
所以,Observer 中的 value 可能是数组,也可能是对象;
Observer 类中 value 也就是 this,是指 observer 实例,并为其添加__ob__
属性
这样,每个对象或数组都有一个 ob 属性,
因此,可在此处为 observer 实例添加 dep 属性
这就相当于为数组或对象本身都增加了一个 dep 属性
这样就可以在对象或数组上,通过value.__ob__.dep
取到 dep
从而当数组数据变化时,通过 dep 中收集的 watcher 实现触发视图更新操作
三,数组依赖收集的位置
对象或数组类型会通过 new Observer 创建 observer 实例,
所以,Observer 中的 value 可能是数组,也可能是对象;
Observer 类中的 value,即 this 指 observer 实例,
为其添加 __ob__
属性,这样每个对象本身或数组就拥有了 ob 属性;
因此,可在此处为 observer 实例添加 dep 属性,
这就相当于为数组或对象本身都增加了一个 dep 属性;
这样就可以在对象或数组上,通过value.__ob__.dep
取到 dep,
当数组数据变化时,可以通过 dep 中收集的 watcher 触发视图更新操作;
四,数组和对象本身做依赖收集
在使用 defineReactive 定义属性时,此时 value 值有可能是数组
对数组的取值会走 Object.defineProperty 的 get 方法
而 get 方法中就会进行依赖收集,如果当前 value 值为数组,就进行依赖收集
所以,当取值时,会对数组和对象本身进行一次依赖收集
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(); // 为每个属性添加一个 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 属性,
当进行观测时,会拿到数组的 observer 实例,即 childOb,childOb.dep
就是 dep;
在页面对数组进行取值时,如{{arr}} 一定会走 get 方法
如果 childOb 有值,就让当前数组把依赖收集起来childOb.dep.depend()
这样就完成了数组的依赖收集
数组本身添加了 dep :
五,数组中嵌套对象(对象或数组)的递归处理
数组中有可能嵌套数组或对象:如[{}]或[[]]
当前只会对数组的外层进行依赖收集,数组中嵌套的数组不会进行依赖收集
注意:此时,数组中嵌套的对象是可以进行依赖收集的
1,数组中嵌套对象的依赖收集原理
例如:arr:[{a:1},{b:2}]
当对 arr 取值时{{arr}},默认会对 arr 进行 JSON.stringify(arr),
JSON.stringify 会取出内部所有属性进行打印输出
即 JSON.stringify 会对内部属性进行取值操作,此时会走 getter,
而 getter 中就会为对象本身和内部属性进行依赖收集
所以,这种情况默认就会进行依赖收集
js
<body>
<div id=app>
{{arr}}
</div>
<script src="./vue.js"></script>
<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 取值,内部会进行 JSON.stringify,就会为对象中的 a 属性做依赖收集;
所以,数组中的对象中的 a 属性更新时,走的就是对象的更新和数组无关
当前数组中的对象中的属性是有 dep 的:
2,数组中嵌套数组的依赖收集实现
js
例如:arr:[[1][2]]
当对 arr 取值时{{arr}},只对外层数组进行依赖收集,内部的数组没有进行依赖收集
所以,arr[0].push直接操作内部数组,是不会触发视图更新的
需要对数组类型做递归依赖收集
css
数组中如果有对象[{}],也需要为对象本身做依赖收集,
因为未来有可能会为对象新增属性,对象本身做依赖收集才可以更新视图
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__,说明是对象,就让 dep 收集依赖(只有对象上才有 __ob__)
current.__ob__ && current.__ob__.dep.depend();
// 如果内部还是数组,继续递归处理
if(Array.isArray(current)){
dependArray(current)
}
}
}
外层数组本身和内层数组都添加了 dep:
3,数组的视图更新
上边已经将数组的依赖进行了收集,目前 arr.push 还不能更新视图,因为没有调用更新方法
所以,当 arr.push 等操作变更数组时,还需要再触发数组的依赖更新(通过 ob 拿到 dep 调用 notify)
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(); // 通过 ob 拿到 dep,调用 notify 触发 watcher 做视图更新
}
});