# Computed
以下面例子为例:
<div id="app">
{{ name }}<br/>
{{bind}}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'AA',
name: '哈哈'
},
watch: {
message:{
handler: function (val) {
console.log(val)
this.name = val + '_Watch'
}
},
name: {
handler: function (val) {
console.log(val)
}
}
},
computed: {
bind: function(){
const result = this.message + '__________' + this.name
return result
}
},
mounted(){
console.log('mounted')
this.message = "message_B"
},
})
</script>
# 源码分析
源码中处理 computed
属性的地方:initState(vn) => initComputed(vm, opts.computed)
function initComputed (vm, computed) {
// 在当有实例上定义一个_computedWatchers属性,保存 conputed Watcher
var watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
var isSSR = isServerRendering();
// 遍历computed
for (var key in computed) {
// 获取computed计算方法
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
if (!isSSR) {
// 实例化 Watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions // { lazy: true }
);
}
// hllComponent-defined computed properties are already defined on the
// hllComponent prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
// 如果当前没有计算属性在 data 属性上,执行 defineComputed 方法
// 配置访问这个属性时要执行就是这个计算方法
defineComputed(vm, key, userDef);
} else {
// 如果当前有计算属性在 data 属性上,就提示错误
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);
}
}
}
}
在 initComputed
也实例化了一个 Watcher
, 此时传进去参数如下:
vm
:当前vue
实例getter || noop
: 当前compunted
的get
方法,也就是应的计算函数noop
:空函数computedWatcherOptions
:{ lazy: true }
回顾一个 Watcher
方法
var Watcher = function Watcher (
vm,
expOrFn, // 这个参数是 conputed 对应的计算方法
cb, // 空函数
options // { lazy: true }
) {
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();
};
与 渲染 Watcher
和 watcher Watcher
不同的是, 此时实例 Watcher
时传的第四个参数 { lazy: true }
所以 options.lazy = true;this.dirty = true
(后面访问 computed
时有用) , 所以computed Watcher
并没有执行最后的 this.get()
方法。
执行完之后回到 initComputed
中,把当前的 Watcher
实例保存到了 vm._computedWatchers
中。 之后执行 defineComputed(vm, key, userDef)
方法
function defineComputed (
target,
key,
userDef
) {
var shouldCache = !isServerRendering();
if (typeof userDef === 'function') {
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
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
如果不是服务端渲染则执行 sharedPropertyDefinition.get = shouldCache; createComputedGetter(key)
,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
}
}
}
此时只是将 computedGetter
方法保存到 sharedPropertyDefinition.set
中,之后再设置 sharedPropertyDefinition.set
方法
sharedPropertyDefinition.set = function () {
warn(
("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
);
};
最后通过 Object.defineProperty
方法设置属性的访问规则
看到这个原来当我们访问 computed
上的属性时,是通过 computedGetter
方法返回真正的值的;
并且如果尝试去修改 computed
上的属性,将会触发上面的 set
中的 warn
提示
到此 compunted
的初始化就完成了,接下就是看看当访问 computed
时做了什么
# 访问computed属性
假设当前组件进入渲染阶段(注意:进入渲染阶段时,执行的是redner Watcher
,并且在渲染完成前,这个 redner Watcher
一直存在于全局属性 Dep.target
中),此时如果有用到 computed
属性并且在访问当前 compuntd
属性时,将会执行下面代码块:
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
}
}
}
上面代码中根据 computed key
从 this._computedWatchers
取出对应的 Watcher
,上文初始化 computed
时 dirty = true
所以会执行 watcher.evaluate()
方法
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.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
};
watcher.evaluate()
方法就会执行 this.get()
方法,首先是 pushTarget(this)
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
注意在执行pushTarget()
前,targetStack
队列中已经存在一个 render Watcher
且 Dep.target
为 render Watcher
pushTarget
执行两步:
将当前
Watcher
添加到targetStack
中,此时targetStack
就存在两个Watcher
, 一个是render Watcher
, 另一个是computed Watcher
。将当前
computed Watcher
添加到全局Dep.targer
中
然后执行 this.getter
方法时就会去执行对应的计算方法,此时就会访问到我们计算方法依赖的 data
属性(上文中的 this.message
和 this.name
属性),然后就会把当前这个 conputed Watcher
添加到属性的收集器 dep
中。执行完 value = this.getter.call(vm, vm)
后,得到计算结果。然后继续往下执行:
popTarget();
this.cleanupDeps();
// popTarget();
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
上面的代码也是做两个事:
首先移出
targetStack
的最后一个元素,也就是当前这个computed Watcher
,移出后targetStack
中还剩下render Watcher
并将
targetStack
最后一个Watcher
保存全局属性Dep.target
中,也就是render Watcher
可以看到 targetStack
是一个栈式的存储结构
执行完 watcher.evaluate();
方法后回到 createComputedGetter
方法中,继续往下执行
if (Dep.target) {
watcher.depend();
}
// Watcher.prototype.depend
Watcher.prototype.depend = function depend () {
var i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
};
我们知道每个属性的收集器 Dep
收集着 Watcher
,但其实每个 Watcher.deps
属性也保存收集了该 Watcher
的 Dep
, 也就是相互收集了。在当前例子中 Watcher.deps
有两个 Dep
,一个是 name
属性的 Dep
,一个是 message
属性的 Dep
上面遍历 deps
, 执行每个 Dep
的 depend()
方法
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
// Watcher.prototype.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);
}
}
}
上文分析到此时的 Dep.target
为当前组件的 render Watcher
,因此上面代码就是将当前 render Watcher
添加到 Dep
中,当然如果当前 Dep
已经包含了这个 Watcher
,将不会重复添加。
以上面例子为例, 因为组件渲染时有用到 name
属性(且在 bind
访问之前),所以 name
属性对应的 Dep
已经有了 render Watcher
,此时就会不重复添加了,而 message
属性的 Dep
中并没有 render Watcher
,所以会添加当前的 render Watcher
, 那么问题来了既然渲染没有用到这个属性,为什么要添加 render Watcher
呢?
# 更新computed依赖
按之前 render Watcher
和 watch Watcher
的更新机制,当依赖的属性更新时,这些 Watcher
将被收集到到一个微任务队列中,并且会按 watcher id
作为升序排序来触发这些 Watcher run
方法
但是 computed Watcher
依赖的计算属性更新时并不会将当前的 conputed Watcher
添加到微任务队列中,以上面例子为例,当 this.message = "message_B"
执行后,meaaage
属性 Dep
此时有两个 Watcher: render Watcher
和 conputed Watcher
,所以会遍历这两个依次执行 watcher.update
方法
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
但是要注意的是computed Watcher
的 this.lazy = true
,根据上面的代码不会执行 queueWatcher(this)
,所以 computed Watcher
不会添加到更新队列中
所以只会执行 render Watcher
的 update
,然后在更新组件的时候会访问到 computed
属性,此时会重新执行计算函数得到新值。这也就可以理解为什么即使计算依赖的属性没有在渲染中用到也会将 render Watcher
添加到 Dep
的原因。
如果 watch
中有监听 computed
属性时,更新机制又是怎样的呢?
通过下面这个例子分析一下:
<div id="app">
{{ name }}<br/>
{{bind}}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'AA',
name: '哈哈'
},
watch: {
// message:{
// handler: function (val) {
// console.log(val)
// this.name = val + '_Watch'
// }
// },
name: {
handler: function (val) {
console.log(val)
}
},
bind: {
handler: function (val) {
console.log(val)
this.name = 'this.name' + val
}
}
},
computed: {
bind: function(){
debugger
const result = this.message + '__________'
return result
}
},
mounted(){
debugger
console.log('mounted')
this.message = "message_B"
},
})
</script>
首先看下初始化 watch bind
的时候做了什么事情:
实现一个
Watcher
实例访问
this.bind
属性之前将当前watch Watcher
保存到targetStack
栈中,然后全局属性Dep.targe
指向当前watch Watcher
因为
this.bind
是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 } } }
得到当前的
compunted Watcher
, 然后通过watcher.evaluate()
得到计算结果,注意evaluate
的代码内容:Watcher.prototype.evaluate = function evaluate () { this.value = this.get(); this.dirty = false; };
通过执行
compunted Watcher
的get
方法得到computed
结果的同时将当前compunted Watcher
保存到targetStack
栈中,然后全局属性Dep.targe
指向当前compunted Watcher
之后还会将
dirty
设为false
(防止后面重复计算)注意计算的时候会访问
this.message
属性,并且此时的全局Dep.target
为当前compunted Watcher
,所以compunted Watcher
会被message
属性收集compunted Watcher
收集完之后,当前compunted Watcher
会被targetStack
推出,将Dep.target
指向targetStack
最后一个Watcher
,也就是watch Watcher
执行完
watcher.evaluate()
,执行watcher.depend();
,上文分析过这个方法的作用就是遍历当前Watcher
的deps
属性,收集当前的Dep.target
保存的Watcher
。 也就意味着这里的watch Watcher
也会被meaaage
属性收集
上面例子中当执行 this.message = "message_B"
时,执行的步骤如下:
- 遍历
message
收集的Watcher
,并执行对应的update
方法,首先执行的是watch bind Watcher
, 执行对应的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) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
先执行
var value = this.get();
访问属性bind
, 此时会执行对应在的computed bind Watcher
,得到computed
结果的同时也会当前的computed Watcher
的dirty
设为false
上一步执行完之后运行
this.cb.call(this.vm, value, oldValue)
执行对应的watch Handler
修改
this.name = 'this.name' + val
,触发对应的watch name handle
最后触发
render Watche
, 此时访问this.bind
,将执行以下语句:
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
}
}
}
注意此时 watcher.dirty
为 false
,所以不会重复执行 computed
请问,而是直接返回 watcher.value
,也就是之前得到的值
# 总结
对于 cmpunted
也会创建一个 Watcher
,并以 key-conputedWatcher
的形式保存到 computedWatchers
中
然后跟 data
属性类似,computed
属性的访问也是通过 Object.defineProperty(target, key, sharedPropertyDefinition);
方法做了一层代理,当前访问 compunted
上的属性时,将被代理到这个 _computedWatchers
属性对应的 computedWatchers
上,然后通过 watcher.evaluate()
执行对应原 compunted
计算方法求值
conputed
依赖的属性会收集 render Watcher
,所以这些依赖的属性更新时会重新渲染组件,在渲染组件的过程中会访问到 computed
属性,再计算对应的方法返回得到值