前言
对于computed属性,几乎每次面试的时候都要去问一遍,之前也就靠着自己的一点业务中使用的理解去说说,但这种还是太浅了,很多时候并不是面试官所需要的答案。所以想着干脆把computed源码阅读一遍,知道它的工作原理,这样才能做到真正的理解。
先看一下初始化吧
先跟着源代码了解Computed是在哪里进行初始化的
function Vue(options) {
...
this._init(options)
}
function initMixin(Vue) {
Vue.prototype._init = function(options) {
...
initState()
...
}
}
function initState() {
...
if (opts.computed) initComputed(vm, opts.computed)
...
}
可以看到,当我们去用new Vue(...)来进行调用时,会进行初始化。会调用到initState()函数,该函数中调用了initComputed进行computed的初始化。
initComputed做了什么?
翻开initComputed可以看到很长的一段代码,为了更容易理解该段代码,我们可以抛去一些输出警告信息以及服务端渲染这块内容,从而提取出一段简短的代码用作分析。后续的代码也都会做相应的处理。
const computedWatcherOptions = { lazy: true }
function initComputed (vm, computed) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
为了让大家能够理解透彻,会拿出些代码做出解释。
1.这块代码是为给每一个computed分配一个Watcher,Watcher在整个Vue中都是个非常重要的类,在后面会贴出部分Watcher的代码进行分析。
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
2.使用函数defineComputed处理computed
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
总结起来initComputed为每一个computed做了下面两件事:
- 分配
Watcher - 使用
definComputed处理
针对这两件事拿出来再做单独分析
分配Watcher做了哪些事?
我们刚才的initComputed代码中可以看到在创建Watcher的时候传递的参数应该是下面这样(注:noop = function(a, b, c){})
new Watcher(vm, function c() { return this.a }, noop, { lazy: true }, undefined)
根据此传参,抛去一些判断,再来看一下Watcher的精简代码
function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
this.dirty = this.lazy = !!options.lazy;
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
}
this.value = this.lazy ? undefined : this.get();
}
根据代码可以看到watcher做了以下几件事
1.将dirty和lazy都赋值为true。
lazy在这里标记着这个watcher需要缓存,而dirty是标记着取缓存的值还是重新计算。可以理解为lazy是个缓存的开关,而dirty标记着缓存还是否有效。
this.dirty = this.lazy = !!options.lazy;
2.将computed的getter缓存下来
this.getter = expOrFn;
3.computed的值会由get获取到,这里根据lazy形成了条件判断,这样初始化的时候就不会去计算获取值,而是后面用到的时候再去从其它地方调用get()。关于get函数我们放到后面再看。
this.value = this.lazy? undefined : this.get();
definComputed做了哪些处理?
看一下definComputed代码:
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function defineComputed (
target,
key,
userDef
) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? createComputedGetter(key): noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
上述代码中主要有这两件事
- 使用
createComputedGetter函数创建一个getter' - 使用
Object.defineProperty修改get为刚创建的getter
createComputedGetter
createComputedGetter里做的事比较多,也涉及到了computed的原理所在,所以要拿出来单独讲讲。
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.由于在initComputed里创建watcher时也把所有创建的watcher都缓存了下来,所以这里会直接根据key来取出响应的watcher。
var watcher = this._computedWatchers && this._computedWatchers[key];
2.这里dirty默认是为true的所以会执行evaluate()进行计算。evaluate()只做了两件事: 1. 使用get()函数获取值, 2. 将dirty属性设为false
if (watcher.dirty) {
watcher.evaluate();
}
evaluate代码
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
}
揭秘get()
先来看一下get的代码
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
...
} finally {
...
popTarget();
}
return value
}
虽然代码不多,但get中关联的其他函数很多,为了方便理解我们举例来说明
例:
var app = new Vue({
el: '#app',
data: {
a: 'a'
},
computed: {
c () {
return this.a
}
},
methods: {
changeA () {
this.a = 'changed'
}
}
})
<div id="app">
<div>{{c}}</div>
<button @click="changeA">Change A</button>
</div>
页面:
结合上面的例子我们来拆分
get的操作
1.pushTarget(this)这个操作时将当前watcher推入栈中,并且让Dep.target变为当前的watcher,也就是c的watcher。(放了方便后续使用,如果是某个值的watcher就用watcher(xx)来简称。)
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
2.调用当前watcher缓存的getter函数,从例子我们可以知道getter就是
function c() { return this.a }
所以会调用a的getter。
a在initData时候已经使用defineProperty做了处理,给设置了getter和setter.先来看看a的getter做了什么,同样是精简后的代码:
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
}
return value
当前Dep.target是watcher(c)的,所以会走到dep.depend()。
再来你看一下depend()的源码
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
上述代码可以得知:
- 因为是从
a的getter走进来的所以this是a Dep.target是watcher(c)。
Dep.target.addDep(this)实际是调用的代码watcher.addDep,然后来看看watcher.addDep的代码:
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
}
这段代码做了两件事:
c的newDeps中加入了a,后面会出来加到deps中a的subs加入了watcher(c)。
所以此时Dep.target(watcher(c))数据结构应该是:
3.最后进行了popTarget()操作,将watcher(c)推出栈,此时Dep.target为当前页面的watcher。然后返回了value。
至此get()函数执行完毕。
将this.dirty设置为false后evaluate执行完毕
继续走createComputedGetter代码会到这里
if (Dep.target) {
watcher.depend();
}
接下来看watcher.depend()做什么
watcher.depend()
看一下watcher.depend()源码:
Watcher.prototype.depend = function depend () {
var i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
分步拆解:
- 此时的
this是watcher(c) - 从上图可以看出来
watcher(c)的deps只有a,所以实际就是拿a去depend() - 当执行
depend()时,get时说过,执行完evaluate后会将Dep.target还原回原来的,所以此时的Dep.target是页面,所以执行完毕后,watcher(page)的deps会加入a,a的subs会加入watcher(page)。
最终页面的watcher结构为:
那么页面又是怎么更新的呢?
刚才所说的initData只说到了a属性的getter方法,当我们点击changeA时,a的值改变,会调用a的setter方法。a的setter里调用了dep.notify()。
看一下notify代码:
Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
notify中取出当前的subs,subs里有什么上面已经说过了,遍历调用subs中watcher的update。
再看一下update函数
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
- 当调用到
c的update时,lazy的值为true,所以会把dirty的值也设为true。 - 当调用到页面的
update时,会触发页面的更新,此时页面引用了c,c的dirty值又变成了true,就会重新从a中取值计算。
从上面我们不难看出,c实际上与页面更新并无关系,实际只取到个计算值的作用,真正与页面更新有关的实际是所依赖的a。
总结
由于没有怎么做过源码的解读,可能有些地方描述的并不是很恰当,或不是很详细,有不明白的可以留言告诉我,在我能力之内的我会一一解答