当我们把一个普通的 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回调会被调用。