前言
computed与watch是我们在vue中常用的操作,computed是一个惰性求值观察者,具有缓存性,只有当依赖发生变化,第一次访问computed属性,才会计算新的值。而watch则是当数据发生变化便会调用回调函数。我们虽然知道vue是这么操作的,但是原理我们又知多少呢?接下来我们来分析一下两者的原理。
Computed
我们知道new Vue()的时候会调用_init方法,此时会调用initState方法,源码如下:
function initState(vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) {
initProps(vm, opts.props);
}
if (opts.methods) {
initMethods(vm, opts.methods);
}
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */ );
}
// 初始化computed
if (opts.computed) {
initComputed(vm, opts.computed);
}
// 初始化watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
在initState方法中调用了initComputed,初始化了计算属性,那么我们看一下initComputed方法:
// 声明一个watchers且同时将_computedWatchers挂载到vm实例上
var watchers = vm._computedWatchers = Object.create(null);
// 在SSR模式下computed属性只能触发getter方法
var isSSR = isServerRendering();
// 遍历传入的computed方法
for (var key in computed) {
// 取出computed对象中key值的每个方法并赋值给userDef
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
// 如果不是SSR服务端渲染,则创建一个watcher实例
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
// {lazy: true}
computedWatcherOptions
);
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
// 如果computed所对应的key值在vue实例中没有重复,则执行defineComputed方法
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else {
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(("The computed property \"" + key + "\" is already defined as a method."), vm);
}
}
}
在initComputed遍历computed,通过userDef去获取当前computed,然后不断的向vm._computedWatchers中添加{lazy: true}
配置的watcher,添加的配置lazy:true代表是computed,接着判断如果在vue实例上没有这个对应的key,我们就执行defineComputed,接下来我们看一下defineComputed源码部分:
function defineComputed(
target,
key,
userDef
) {
var shouldCache = !isServerRendering();
// 一般都是function
if (typeof userDef === 'function') {
// 判断是否是服务端渲染,不是的话则走createComputedGetter
sharedPropertyDefinition.get = shouldCache ?
createComputedGetter(key) :
createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get ?
shouldCache && userDef.cache !== false ?
createComputedGetter(key) :
createGetterInvoker(userDef.get) :
noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
);
};
}
// 数据劫持,当进行获取computed的话,则执行sharedPropertyDefinition的get方法
Object.defineProperty(target, key, sharedPropertyDefinition);
}
// 默认会走这个函数去创建
function createComputedGetter(key) {
return function computedGetter() {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
// 如果订阅者的值发生了改变,则dirty会变为true,此时会执行evaluate
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
Watcher.prototype.evaluate = function evaluate() {
// 调用this.get此时会触发数据劫持的getter方法,进行重新计算computed的值
this.value = this.get();
// 将dirty变为false,代表此时是新值,下次获取的时候不需要重新计算
this.dirty = false;
};
/**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function get() {
// 将全局的Dep.target指向当前watcher
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
在defineComputed去设置对应的getter与setter,最终通过Object.defineProperty进行绑定,对于getter如果不是服务端渲染的话则会走createComputedGetter来进行设置其getter。在createComputedGetter函数中,获取对应的watcher,如果watcher.dirty为true的话则去触发evaluate方法,在evaluate方法中调用this.get(),去触发this.getter.call,其实是执行计算属性的方法,比如:
computed:{
fullName() {
return this.firstName + this.lastName;
}
}
执行fullName回调,执行过程中,又会对firstName和lastName取值,触发了它俩的getter,所以firstName和lastName的dep会搜集当前Dep.target所指向的computed watcher。同样firstName和lastName的dep也会被添加到computed watcher中,此时fullName计算属性watcher保留着两个dep。
在createComputedGetter中watcher.value拿到返回的value;接着将watcher.dirty值改为false,表示当前已为计算属性取过值了,只要firstName和lastName值不变,以后就不需要重新计算取值,达到一个缓存的效果。
在createComputedGetter中Dep.target指向渲染watcher,执行watcher.depend(),目的:给firstName和lastName的dep添加当前的渲染watcher,这样firstName和lastName值改变就会更新组件了,此时firstName和lastName的dep有两个watcher。
当执行更新时,此时也就触发了setter对应的方法,此时会触发render watcher与computed watcher对应的update方法,源码如下:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
如果为computed watcher则会将this.dirty变为true,如果是render watcher则会调用queueWatcher方法。
dirty的作用其实就是只在相关响应式属性发生改变时才会重新求值。如果重复获取计算属性的返回值,只要响应式属性没有发生变化,就不会重新求值。也就是说当响应式属性改变时,触发响应式属性的setter,通知Computed Watcher将dirty置为false;等再次获取时,会获取到最新值,并重新给响应式属性的dep.subs添加Watcher
总结
在初始化阶段调用对所有的计算属性执行defineComputed方法,对挂载到vue实例上的计算属性进行拦截,并设置对应的存取描述符
get(createComputedGetter函数)和set。当首次触发拦截器中对应的
get时会进行依赖收集,将computed watcher 与 render watcher添加到计算属性中依赖属性dep实例的订阅者列表中。当计算属性所依赖的属性发生变化时,会通知dep实例中所有的订阅者进行更新,其中包含computed watcher与render watcher,当computed watcher执行update方法时,会执行dirty变为true,使得下次访问计算属性时,需要进行重新计算。当render watcher执行update方法时,会调用对应的run方法。
Watcher
初始化
首先初始化阶段是在initState阶段,去调用initWatch,那么我们分析下initWatch这个方法。
function initWatch(vm, watch) {
for (var key in watch) {
var handler = watch[key];
// 如果是数组的话则挨个调用createWatcher方法
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
// 如果不是数组的话,则单个调用createWatcher
createWatcher(vm, key, handler);
}
}
}
在initWatcher中主要是对所有watcher调用createWatcher方法,那么我们接着分析。
function createWatcher(
vm,
expOrFn,
handler,
options
) {
if (isPlainObject(handler)) {
// 处理参数
options = handler;
handler = handler.handler;
}
// handler为方法名
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
那么接下来我们分析一下vue实例的$watch方法:
// $watch
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
// 如果通过 this.$watch 设置的监听,则会执行 createWatcher 获取回调函数
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
// 此时 user 为 true,说明创建的 Watcher 是一个 User Watcher,将user Watcher进行标记
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
// 如果immediate为true则立即执行回调函数
if (options.immediate) {
var info = "callback for immediate watcher \"" + (watcher.expression) + "\"";
pushTarget();
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
popTarget();
}
return function unwatchFn() {
watcher.teardown();
}
};
依赖收集
vue实例的$watch方法是创建一个User Watcher,并判断immediate是否为true,如果为true则立即执行一次回调函数。watch的初始化结束。
那么接下来是依赖收集阶段。在初始化过程中会为每个watch创建一个User Watcher,而创建过程中会对被监听属性做依赖收集。在User Watcher的创建过程中:
var Watcher = function Watcher(
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
this.value = this.lazy ?
undefined :
this.get();
};
User Watcher除了user属性为true外,还有deep、async两个属性,这两个属性都是watch的配置项。对于deep属性,是我们在设置watch时可以有选择的去配置,在user watcher回调用getter获取对应的属性进行赋值,因为lazy为false所以最终调用this.get()方法,也就是说只要不是computed watcher就都会调用this.get方法,接下来我们看一下this.get方法
/**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function get() {
// 将全局的Dep.target指向当前watcher
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
当前Watcher入栈,执行this.getter(每类Watcher的getter属性不同),执行traverse 方法(只有 deep为true的User Watcher才会执行)
当前Watcher出栈,处理Watcher的deps属性,返回 value。当入栈时,此时会将Dep.target设置为当前的user watcher,执行this.getter时会去触发监听属性的getter,将user watcher添加到dep的订阅者列表中,如果deep为true的话,就执行traverse,traverse的逻辑如下:
function traverse(val) {
_traverse(val, seenObjects);
seenObjects.clear();
}
function _traverse(val, seen) {
var i, keys;
var isA = Array.isArray(val);
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
var depId = val.__ob__.dep.id;
if (seen.has(depId)) {
return
}
seen.add(depId);
}
if (isA) {
i = val.length;
while (i--) {
_traverse(val[i], seen);
}
} else {
keys = Object.keys(val);
i = keys.length;
while (i--) {
_traverse(val[keys[i]], seen);
}
}
}
traverse逻辑为如果被监听的属性是一个对象,则把对象的所有属性都访问一遍,从而触发所有属性的依赖收集;将User Watcher添加到每个属性的dep.subs中,这样当某个属性修改时,会触发属性的setter,从而触发watch回调。
触发回调
当对应的值发生变化时,此时会触发setter,通知所有订阅者调用update方法。
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
如果 User Watcher的sync属性为true,立刻执行run方法;如果sync属性为false,通过queueWatcher(this) 在下一次任务队列中执行User Watcher的 run 方法,run方法如下:
Watcher.prototype.run = function run() {
if (this.active) {
var value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
if (this.user) {
var info = "callback for watcher \"" + (this.expression) + "\"";
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
对于User Watcher的run方法,首先会调用this.get()重新让被监听属性做依赖收集,并获取最新值;如果最新值和老值不想等,调用回调函数,并将新老值传入,执行对应的回调函数。
总结
在初始化阶段,会为每个watch创建一个User Watcher,如果watch的immediate为true,会马上执行一次回调;创建User Watcher过程中会获取一次被监听属性的值,从而触发被监听属性的getter方法,将User Watcher添加到被监听属性的Dep实例中。
当被监听属性发生改变时,通知User Watcher更新,如果watch的sync为true,会马上执行watch的回调;否则会将User Watcher的update方法通过nextTick放到缓存队列中,在下一个的事件循环中,会重新获取被监听属性的属性值,并判断新旧值是否想等、是否设置了deep为true、被监听属性是否是对象类型,如果成立就执行回调。
Vue源码系列文章: