当我们把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项时,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty把这些 property 全部转为 getter/setter。在这之后,每次我们对data对象里的property赋值时,相应的setter方法会被调用,而setter方法里包含了通知vue更新视图的代码,因此接下来vue就会根据最新的data生成新的虚拟dom树,和旧的树做比较并更新有变化的地方。
那么,是只要对data里任何的property赋值都会触发vue组件的更新流程吗?
其实不是的,只有对被 ''用'' 到了的property进行赋值才会触发变更检测。假如data中有属性prop1和prop2,而组件的template中只绑定了prop1,这时候我们对prop2赋值就不会触发变更检测,因为template和prop2没有依赖关系。
以上这些都是vue官网对响应式原理的描述。而作为一个好奇的程序猿,自然会好奇vue中对这种依赖收集和变更检测的触发具体是如何实现的,我就去研究了一下vue的源代码。
Watcher 和 Dep
通过阅读源代码发现每个组件实例都会绑定一个watcher对象,保存在组件实例的_watcher属性中。
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
//vm是创建watcher对象时传递进来的组件实例
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
......省略代码
};
而组件中data和props里的所有属性都会绑定一个dep对象。
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
// 创建Dep对象实例,下面属性的setter方法和getter方法能通过闭包作用域访问到该对象
var dep = new Dep();
......省略代码
var property = Object.getOwnPropertyDescriptor(obj, key);
......省略代码
var childOb = !shallow && observe(val);
// 为属性设置getter,setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
......省略代码
if (Dep.target) {
// 在getter方法中调用Dep.depend()让该属性成为Dep.target的依赖,Dep.target的值为watcher对象
dep.depend();
......省略代码
}
......省略代码
return value
},
set: function reactiveSetter (newVal) {
......省略代码
// 在setter方法中调用dep.notify()通知被依赖的wathcer对象调用wathcer.get()方法
dep.notify();
}
});
}
当组件初始化或更新视图时会把Dep.target的值(在此可以吧Dep.target理解成一个全局变量),设置成组件绑定的wathcer对象。而组件初始化要生成虚拟dom树就必定要读取绑定到template中的property的值,那么getter方法就会被调用,在getter方法里会调用dep.depend()方法。
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
最终,Dep.target保存的wathcer对象会被push到dep.subs数组中。而在属性的setter方法里,dep.notify()方法被调用。该方法会遍历dep.subs数组中的wathcer对象并调用watcher.update()方法。
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
......
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this); // 把watcher放到异步队列中
}
};
最终,与组件绑定的watcher对象的getter回调方法会被调用,该getter回调方法是在实例化对象时作为构造参数传入的。组件级wathcer的getter方法是一个叫updateComponent的方法,这会导致组件检测变化并更新视图。
Watcher.prototype.get = function get () {
pushTarget(this); // 把Dep.target的值设置为本wathcer
......省略代码
try {
value = this.getter.call(vm, vm);// 调用getter回调方法
} catch (e) {
......省略代码
} finally {
......省略代码
}
return value
};
......省略代码
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
最后我们不难想到那些没被用到的data属性因为在组件初始化时其getter方法没被调用,所以其绑定的dep.subs数组中没有组件的wathcer对象, 所以即使对其赋值会导致dep.notify()的调用也不会触发vue组件的变化更新。(vue组件在初始化视图或在视图更新时会调用pushTarget(this)把Dep.target设置为组件的wathcer,更新完成后会调用popTarget()把Dep.target设置为前一个值或null,所以在此之后再调用dep.depend()并没有什么效果)
computed属性和watch属性的实现原理
computed属性和watch属性实现原理的核心也是Watcher和Dep,先来具体看一下computed属性。
computed属性
组件在初始化时会遍历computed属性,并为每一个computed属性绑定一个watcher对象,而创建wathcer对象时传入的getter回调即为computed属性的方法。
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 (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
......省略代码
}
当我们要读取computed属性的值时,watcher.get()方法调用。该方法会先调用pushTarget(this)把Dep.target的值设置为当前wathcer, 然后调用getter回调(即computed属性的方法)。如果getter回调方法体内有读取data属性或props属性的值,则它们会在自己的getter方法里调用dep.depend(),然后这些属性和computed属性就会形成依赖关系。而wathcer的getter回调函数返回的值会被保存到wathcer.value中,并且会把wathder.darty置为false。
computed级watcher和组件级wathcer不同的是computed级watcher.lazy属性为true,这意味着wathcer.get()调用后不会马上调用wathcer的getter回调,而是会先检测wathcer.darty是否为true,若不为true则立马返回wathcer.value(上一次调用getter回调返回的值),若为true则重新调用getter回调计算新的值并保存在wathcer.value中。
当对和computed属性有依赖关系的data和prop属性进行赋值操作后,dep.notify()调用,这会导致wathcer.dirty被置为true。这样当下次要获取computed属性的值时,computed方法会重新计算出新的值并保存到wathcer.value中。
所以不是每次获取computed属性的值时computed属性的方法都会执行,而是当computed属性的方法依赖的属性被重新赋值后才会重新执行。
watch属性
同样的在初始化组件时会遍历wwath属性被为每一个watch属性创建一个watcher对象,并在创建watcher对象时传入watch属性的名称和watch回调方法, 然后把该wathcer对象push到相应的data属性和prop属性的dep.subs中。当dep.notify()调用后,watch回调会被调用。






网友评论