版本:vue@2.6.10
0-计算属性的诞生(initComputed)
初始化 options中的 computed 属性。
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
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)
}
}
}
}
1-在实例上新建一个 _computedWatchers 属性用于存放computed的watcher实例
2-获取computed的getter属性
3-判断环境是否是ssr
-非ssr的话,用computed的getter属性生成watcher实例,并将computed对应的watcher实例存到实例上的_computedWatchers中,
vm._computedWatchers[key] =new Watcher(vm, getter, noop, computedWatcherOptions)
4-对vm上的属性进行校验,因为我们要把computed属性也挂到vm实例上,所以computed名称不能与props,data中的属性值相同。
5-当校验通过,对computed的getter进行劫持,根据是否是服务端渲染,进行不同的劫持,
-createComputedGetter //非服务端渲染
-createGetterInvoker //服务端渲染
6-最后将computed对应的key添加到vm实例上
createComputedGetter
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
1-对computed的属性劫持,是每当使用computed属性时(get),都会调用我们的劫持属性,
劫持属性会对computedWatcher进行一些操作,在最后分析。
2-计算属性的watcher的dirty属性,是一个标志,当为false时,说明计算属性依赖的数据没有更新,
只让依赖数据收集渲染watcher,将value直接返回,只有依赖数据更新时,dirty才为true,才重新计算值。
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
}
}
1-服务端渲染,对computed的劫持,没有watcher????????????
computedWatcher
watchers[key] = new Watcher(
vm, //实例
getter || noop, //computed属性调用的函数
noop, //空函数
computedWatcherOptions //计算属性的特性值 { lazy: true }
)
Watcher 构造函数
由于我们的watcher构造函数,被运用于三种watcher,我们的computedWatcher只走其中的一部分,因此简化为:
var Watcher = function Watcher (
vm, //实例
expOrFn, //computed属性调用的函数
cb, //未传
options, //{ lazy: true }
isRenderWatcher //未传
) {
this.vm = vm;
vm._watchers.push(this);
this.lazy = !!options.lazy;
this.cb = cb;
this.id = ++uid$1; // uid for batching
this.active = true;
this.dirty = true
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
this.getter = expOrFn;
this.value = this.lazy
? undefined
: this.get();
};
1-我们在初始化computed时在vm._computedWatchers保存的watcher就是上面这些属性
(计算属性的watcher主要标志是dirty,lazy,初始化时都是true)。
3-由于我们对computed函数的get做了劫持处理,在使用computed属性时,
调用的是createComputedGetter的返回函数,会将我们保存在vm._computedWatchers中对应的computedWatcher取出来。
由于我们的watcher.dirty是true,运行watcher.evaluate()
watcher.evaluate()
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};
1-运行this.get是获取计算属性的最新值,然后设置计算属性watcher的状态dirty为false
【证明这个计算属性现在是最新的数据。】
Watcher.prototype.get
Watcher.prototype.get = function get () {
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
};
1- 运行计算属性watcher的get方法,
2- 将计算属性的watcher赋值给Dep.target
3- 运行计算属性的函数,将值赋值给value
- 如果在计算属性的函数内,依赖了data,props,methods的话,
由于Dep.target是计算属性的watcher,那么在收集依赖时收集的也是计算属性。【关于依赖互相收集的过程后续单独分析】
4- 最后将计算属性watcher出栈,Dep.target还原为外层watcher,
5- this.cleanupDeps()
-对depIds newDepIds newDeps deps进行维护。防止重复收集?????
在运行完get后,设置dirty为false,然后继续运行createComputedGetter。因为在get函数中,我们的计算属性watcher已经入栈【成为Dep.target】又出栈【Dep.target又变为外层的渲染watcher】被依赖的响应式数据收集,所以,现在的Dep.target是外层的渲染watcher,继续运行
watcher.depend()【此时的watcher是计算属性watcher】,在depend函数中,是调用我们watcher收集的dep的depend函数,作用是调用Dep.target的addDep函数,addDep的作用是watcher将dep收集并且dep同时将watcher收集。【这是一个以来相互收集的过程,详情在另一篇。】这样,渲染watcher和计算属性所依赖的数据也进行了相互收集。
最终结果是计算属性所依赖的数据(data,props,methods)即收集了计算属性watcher,也收集了渲染watcher。
当计算属性所依赖的数据发生变化时,会通知收集的watcher【计算属性watcher,渲染watcher】更新(watcher.update()),
Watcher.prototype.update()
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
1-由于的计算属性的watcher的lazy属性为true,所以计算属性的watcher运行update只是更新了一下自身状态,
将dirty设置为true【证明这个计算属性脏了】。
2-渲染watcher的更新是queueWatcher(this)。
queueWatcher(watcher)
var MAX_UPDATE_COUNT = 100;
var queue = [];
var activatedChildren = [];
var has = {};
var circular = {};
var waiting = false;
var flushing = false;
var index = 0;
/**********/
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) { //防止同一个watcher入栈多次进行更新
has[id] = true; //收集入栈的watcher的id,进行标记
if (!flushing) { //判断是否已经进行更新,flushing为false是未在更新
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
if ( !config.async) {
flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue);
}
}
}
1-queueWatcher的作用是将watcher入栈进行统一更新。
2-当前还没有进行更新的话,就将watcher放入更新队列里。
3-在更新时会对watcher进行排序后更新,排序是为了保证: 【flushSchedulerQueue内为执行更新】
-组件从父级更新到子集
-userWatcher在renderWatcehr前更新
-当某个组件在父组件的观察期间被销毁,他的观察者就可以不用执行
4-已经开始更新的话,由于我们的watcher是进行排序后更新的,将我们的watcher插入排序的队列里
5-初始waiting为false,设置为true,config.async【异步更新,初始为true】运行
nextTick(flushSchedulerQueue)
nextTick
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
1-nextTick将我们的flushSchedulerQueue进行包裹后放入callbacks里。
2-初始pending为false,设置pending为true执行timerFunc()
3-当运行环境支持Promise时,timerFunc是一个promise函数,执行flushCallbacks函数,
将执行我们放进callbacks的函数【也就是经过包装的flushSchedulerQueue】
flushSchedulerQueue
function flushSchedulerQueue () {
currentFlushTimestamp = getNow(); //获取当前时间
flushing = true; //是否已经开始更新的标志
var watcher, id;
// Sort queue before flush.
// This ensures that:
// 1\. Components are updated from parent to child. (because parent is always
// created before the child)
// 2\. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3\. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort(function (a, b) { return a.id - b.id; }); //对队列里的watcher进行排序
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) { //判断是初始挂载还是更新 更新运行beforeUpdate钩子
watcher.before();
}
id = watcher.id;
has[id] = null; //删除记录中已经执行更新的watcher
watcher.run();
// in dev build, check and stop circular updates.
if ( has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\"")
: "in a component render function."
),
watcher.vm
);
break
}
}
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
1-最终是运行watcher.prototype.run函数再次进行收集挂载【其中包括虚拟dom的对比更新,另一篇学】
vue官网对于 computed 属性的列举
// 仅读取
aDouble: function () {
return this.a * 2
},
// 读取和设置
aPlus: {
get: function () {
return this.a + 1
},
set: function (v) {
this.a = v - 1
}
}