大家好,新人一个,初次写博客还请大家多多关照。
对于Vue的响应式,想必大家都有所了解,在Vue响应式数据中,computed 是比较特殊的响应式数据,它们可以监听使用到的 数据,数据 改变 computed 的数据也会重新计算。
今天主要是讨论 computed 实现原理 。 computed 在内部主要是运用 Watcher 和 Dep 构造函数进行收集依赖和派发更新。
咱们先来看看 Watcher 和 Dep 源码。
var uid = 0;
/**
* dep 就是用来给每个数据做个标记,可以用来收集数据和派发更新
*/
var Dep = function Dep () {
this.id = uid++;
//给每一个 Dep 打一个标记。这里需要说明一下,vm中data的每个数据进行初始化的时候都会调用this.dep = new Dep(),每一个数据都会有dep属性。
//(通过Observer 构造函数进行数据初始化,这里就不多说了,大家感兴趣可以看一下源码)
this.subs = []; // 这个subs收集的是watcher,派发更新的时候遍历每个watcher调用update方法
};
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);//这个方式是用来收集watcher的(每个computed构建出来的Watcher实例)
};
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this); // 这里的方法是Watcher原型上的addDep方法,请看下面
}
};
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.depIds.has(id)) {
this.depIds.add(id); //将依赖的每个dep,添加到 watcher 的 deps集合中,完成数据的收集
this.depIds.push(dep);
}
};
Dep.prototype.notify = function notify () {
// 用来出发watcher的更新,每当数据改变,每个数据的dep就会出发motify方法,将收集的watcher逐个进行数据更新,
//这里就是为什么computed知道依赖改变了,然后就会自动更新。其实不是computed知道依赖改变的,而是依赖改变以后出发computed更新。
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
Dep.target = null;
var targetStack = [];
function pushTarget (target) {
targetStack.push(target); // 将 Dep 原型 上的 Target 设置为 watcher
Dep.target = target;
}
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
// vm 是 Vue 实例
// expOrFn 是传过来的数据 get 函数
// cb 是回调函数
var Watcher = function Watcher (vm,expOrFn,cb,) {
this.vm = vm;
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.deps = []; // deps 是用来收集 依赖数据节点 的集合
this.depIds = new _Set();
this.value = this.get(); // 首次执行get方法(get也就是expOrFn),初次收集依赖是在这个环节
};
Watcher.prototype.get = function get () {
pushTarget(this); // 将 Dep 的 target 设置为当前 watcher,这个函数在 Dep 那块最后面
var value;
var vm = this.vm;
value = this.getter.call(vm, vm);
popTarget();
this.cleanupDeps(); 将 Dep 的 target 设置为当前 空,这个函数我会放在后面
}
return value
};
/**
* 添加依赖的方法,重点!!! 着重看一下 (我进行了一些简化,便于理解)
*/
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.depIds.has(id)) {
this.depIds.add(id);
this.depIds.push(dep);
}
if (!this.depIds.has(id)) {
dep.addSub(this);
} //这步是将自身(也就是watcher)添加到dep中的watcher集合,派发数据更新时用到
};
/**
* 更新数据的方法,在派发更新的时候会用到。 computed 更新数据的时候,用 dep 的 notify 方法进
* 行更新数据,更新数据主要调用的是 run 方法
*/
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
/**
* 在这个阶段主要是运行get方法,拿到数据 (简化以后的代码)
*/
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
this.value = value
}
};
/**
* 深度收集依赖,computed 可以收集 computed 数据就是依靠这个方法
*/
Watcher.prototype.depend = function depend () {
console.log(this.deps)
var i = this.deps.length;
while (i--) {
this.deps[i].depend(); //注意这里的 depend 方法是 Dep 原型上的方法,不是Watcher 的法
}
};
大家可以仔细看一下我的注释,进行了每个步骤的描述,当然都只是我自己的理解,有不对的地方还请大家多多指教。
现在来细说一下收集依赖的流程。当初始化Vue实例以后, 在初始化 computed 阶段,vue 会对每个 computed 进行运算和收集依赖。当 computed 初始化的时候会依靠当前的 computed 生成一个 Watcher,并且将 getter 方法传入。
function initComputed (vm, computed) {
var watchers = vm._computedWatchers = Object.create(null);
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
)
}
这时候已经有了watcher实例。 当 watcher 初始化,会调用 get方法(收集依赖在这里),
Watcher.prototype.get = function get () {
pushTarget(this); // 将 Dep 的 target 设置为当前 watcher,这个函数在 Dep 那块最后面
var value;
var vm = this.vm;
value = this.getter.call(vm, vm);
popTarget();
this.cleanupDeps(); 将 Dep 的 target 设置为当前 空,这个函数我会放在后面
}
return value
};
get执行得时候会调用pushTarget方法(这个方法就是将当前的全局的 Dep.target 设置成当前运行的 watcher ),然后会进行 getter(),getter() 执行的时候会触发 每个数据 的 get 方法,请看get源码
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) { //判断当前执行是否为watcher,也可以说是否为computed运算的数据
dep.depend(); // 收集到watcher里面的deps里面
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
})
这里的 get 方法会首先判断是否有 Dep.target,这个判断最终目的是判断当前获取数据的是否是watcher,如果是,就会调用 Dep 的 depend 方法进行收集,这样 watcher 就会把运算的每个数据的dep收集到 自己的 deps 属性中,完成了数据依赖收集。
咱们再来细说一下Dep 的 depend 方法,这个方法主要是调用的 Watcher 里面的 addDep方法, 这个方法进行的是双向数据收集,也就是说 watcher 会收集 deps, 同样 dep 也会收集 watcher 集合,存到dep 的 subs 属性中, 每当 dep 的数据更新时,就会将subs的每个 watcher 进行 update ,这样就完成了数据更新,这就是 computed 的实现原理。
简单点说,就是 computed 在运行的时候,首先将全局的 Dep.target 设置当前 computed 的 watcher,然后在运行 computed 代码,里面用到的数据会调用它们自己的 get 方法,在 get 方法里,他们会将自己的 dep.id 存到当前的 Dep.target 里,然后还要将当前的 Dep.target(也就是当前的 watcher) 存到自己的 dep.subs 属性 中,每当自己数据更新触发 set 方法,就会把自己 dep.subs 中的每个watcher 拿出来进行数据更新,从而更新 computed 。