前言
对于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
。
总结
由于没有怎么做过源码的解读,可能有些地方描述的并不是很恰当,或不是很详细,有不明白的可以留言告诉我,在我能力之内的我会一一解答