主题
依赖收集
依赖收集
依赖收集是Vue
数据响应式的核心, 初始化时候会进行依赖收集, 等到数据变更的时候再进行派发更新, 数据响应式就闭环了, 但是需要注意, 再vue2
中, 对象和数组的依赖收集方式是不一样的
对象的依赖收集
1. 大体逻辑
首先在首次渲之前会执行_render
函数, 执行render
函数的时候, render
函数内部的变量需要从vm上取值,由于在前面,已经通过observe
对data
内的值进行观察, 取值的时候会执行对应属性的get
函数, 改变值的时候会触发对应属性的set
函数, 因此在改变值的时候需要知道那部分是依赖这个属性的, 就需要在取值的时候对依赖进行收集, 才能在值改变的时候触发这部分依赖的watcher
内的exprFn
函数重新执行
在vue
中, 需要有一个Dep
来存放watcher
, Dep
的角色是一个观察者, 在依赖变更的时候通知对应的watcher
更新依赖
那么问题是如何知道当前哪个属性正在取值呢? 由于js是执行时单线程的,同一时间只会有一个属性在取值, 因此在执行render
函数的这段时间内, 就知道时哪些在取值
2. pushTarget与popTarget
observe/wathcer.js
js
import { popTarget, pushTarget } from "./dep";
class Watcher{
// ....
get() {
pushTarget(this);
this.exprFn();
popTarget();
}
}
export defaultWathcer
3. Dep
Dep.js
js
let uid = 0;
class Dep {
static target = null;
constructor() {
this.uid = uid++;
this.subs = [] // 用于存储wathcher
}
// 依赖收集
depend() {}
}
export default Dep;
let stack = []; // 用一个栈维护当前正在取值的对象对应的watcher
export function pushTarget(target) {
Dep.target = target; // target为Dep的静态属性, 同一时间只会有一个
stack.push(target); // 取值的的时候将当前的watcher放入栈中
}
// 执行完后会调用popTarget方法
export function popTarget() {
stack.pop(); // 将执执行完成的watcher从栈中移除
Dep.target = stack[stack.length - 1];
}
4. 依赖收集与派发跟新流程
在observe内部对对象进行依赖收集与派发跟新操作
js
// 定义响应式数据
function defineReavtive(obj, key, val) {
// 递归观测每一层
observer(val);
const dep = new Dep(); // 为每一个val创建一个dep实例
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) { // 在wathcer中调用了get方法后, Dep.target肯定是存在的
// 依赖收集
dep.depend();
}
return val;
},
set(newVal) {
if (val === newVal) return;
// 有可能设置的新值也是一个对象, 需要对设置的新值进行观测
observer(newVal);
val = newVal;
// 派发更新
dep.notify();
},
});
}
当get
中调用dep.depend()
时候, 就是对val
进行依赖收集
js
// Dep内部-*/`14
depend() {
this.subs.push(Dep.target);
}
当set
中调用dep.notify()
时候, 就是派发跟新, 需要想重新执行watcher中的exprFn
函数, 由于这里为渲染Wathcer,因此执行的函数为updateCompunent
js
// Dep内部
// 派发更新
notify() {
this.subs.forEach((wathcer) => wathcer.update());
}
那么在watcher内部应该就有一个update
方法
js
// Watcher内部
// 更新页面
update() {
this.get()
}
对象的依赖收集流程大致完成, 当然还存在一些问题, 后面再说, 先来看数组的依赖收集
数组的依赖收集
在observe
中, 区分了对象与数组, 若是数组会递归observe每一项数组元素, 递归过程中就做了依赖收集, 但数组还有一个操作就是数组特有的7个方法, 这7个方法同样也需要进行依赖收集
举个例子: 一个数组,用了push
方法后, 新增了一项元素, 这个数组变化了,所以在push
方法内可以进行派发更新, 但是这里面没有dep
, 要调用dep.notify
, 可以将dep
添加到当前数组的__ob__
身上
js
// 对数组重写方法
methodsToPatch.forEach((method) => {
arrayMethods[method] = function (...args) {
// 先调用原始的方法
const res = arrayProto[method].apply(this, args);
// this表示当前数组 value.push(), 这里的value会走到这里
const ob = this.__ob__; // this为数组本身
let inseted;
switch (method) {
case "push":
case "unshift":
inseted = args;
break;
case "splice":
inseted = args.slice(2);
break;
default:
}
// 对新增属性进行观测
if (inseted) ob.observerArray(inseted);
ob.dep.notify(); // 数组方法派发更新
return res;
};
});
在observer
中, 在对一个值为数组的key
取值时候,进到defineReavtive(obbj,key, val)
中后, val
就是一个数组, 这个数组身上有了__ob__
属性, 因此可以针对val.__ob__.dep
进行依赖收集与派发跟新了
js
// 定义响应式数据
function defineReavtive(obj, key, val) {
// 递归观测每一层
observer(val);
const dep = new Dep();
const childOb = val.__ob__; // val为数组本身
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend(); // 数组本身也会被取值,也需要进行依赖收集
}
}
return val;
},
set(newVal) {
if (val === newVal) return;
// 有可能设置的新值也是一个对象, 需要对设置的新值进行观测
observer(newVal);
val = newVal;
dep.notify();
},
});
}
到这里, 数组的依赖收集也就完成了
解决依赖重复的问题
问题描述
上述对象和数组的依赖收集存在一个问题, 若一个值在模板中多次使用, 就会被多次收集依赖, 但对于同一个值来说, 依赖都一样,多次收集依赖会造成性能的浪费, 解决方案如下:
- 每一个依赖都会有一个
Dep
实例, 因此给每一个dep
添加一个ID
,用于去重 - 每一个
dep
中的存储是watcher
也应该是唯一的, 因此每一个wathcer
也应该有一个唯一的ID
- 一个
dep
身上会有多个watcher
, 同样, 一个watcher
也属于多个dep
, 因此dep
与watcher
的关系为多对多
依赖收集时候的去重策略
js
// Dep内部
// 依赖收集方法进行改造
depend() {
// this.subs.push(Dep.target); // 这样同一个dep中可能会存再多个一样的watcher, 浪费性能
// Dep.target 就是当前的watcher
Dep.target.addDep(this);
}
js
// Wathcher内部
class Watcher {
constructor() {
// ....
this.uid = uid++; // 每一个组件都有一个渲染watcher, 这是唯一标识
this.depsIds = new Set(); // 一个watcher上面有多个dep, 用于去重
this.deps = []; // 当前watcher属于哪些dep
}
// 添加dep
addDep(dep) {
const depId = dep.uid;
if (!this.depsIds.has(depId)) { // 去重操作
this.depsIds.add(depId);
this.deps.push(dep); // 将当前watcher与dep建立1对多的关系
dep.subs.push(this); // dep与watcher也建立1对多的关系
}
}
}
到这里, 依赖收集和派发跟新的核心逻辑就写完了, 但依然还存在一些效率问题, 后面再来解决