这道考察computed属性的题蛮有意思的。
不仅仅考察了computed,而且还考察了vue的依赖收集以及脏检查。
computed : {
foo() {
if(this.a>0){ return this.a}
else { return this.b + this.c }
}
}
data() {
a: 1,
b: 1,
c: 1,
}
众所周知,首次a,b,c均为1时,foo()返回值为1。
以foo()返回值为1作为起始态,独立的执行下面以下3个操作,vue会如何计算foo呢?
- 如果此时this.a = 0,foo()如何计算?
- 如果此时this.b = 2,foo()如何计算?
- 如果a的初始值为-1,执行this.a = 1,foo()如何计算?
目录
- 执行表现
源码分析
- createComputedGetter
- 关键的watcher.js
- 关键的dep.js
基于源码分析拆解执行表现
- 初始化a,b,c均为1时,foo()如何计算?
- 如果此时this.a = 0,foo()如何计算?
- 如果此时this.b = 2,foo()如何计算?
- 如果a的初始值为-1,执行this.a = 1,foo()如何计算?
- 一句话总结
执行表现
- 如果此时this.a = 0,foo()如何计算?
- 如果此时this.b = 2,foo()如何计算?
- 如果a的初始值为-1,执行this.a = 1,foo()如何计算?
如果此时this.a = 0,foo()如何计算?
foo()的返回值会从this.a变为this.b+this.c,2。
vue会重新执行一遍evaluate,得到返回值this.b+this.c。
如果此时this.b = 2,foo()如何计算?
foo()的返回值仍旧为this.a,1。
vue会跳过evaluate的阶段,直接得到返回值this.a。
如果a的初始值为-1,执行this.a = 1,foo()如何计算?
foo()的返回值会从this.b+this.c变为this.a。
vue会重新执行一遍evaluate,得到返回值this.a。
为什么会是这样的呢?是否执行evaluate的条件是什么?
为什么a的初始值为-1了也可以重新evaluate?
源码分析
对于this.b = 2,vue跳过evaluate阶段,直接得到返回值this.a,是如何优化的呢?
下面我们来看源码:
源码地址:state.js
computed相关的有三个非常重要的函数:
- createComputedGetter(脏检查,依赖收集)
- 关键的watcher.js(Watcher)
- 关键的dep.js(依赖)
先来看看最最核心的代码
// 脏检查, 执行计算
if (watcher.dirty) {
watcher.evaluate()
}
// Dep更新依赖
if (Dep.target) {
watcher.depend()
}
下面再看具体的源码
createComputedGetter
- 脏检查, 重新计算 对__computedWatcher中的具体的属性做检查
Dep更新依赖
function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 脏检查, 执行计算 if (watcher.dirty) { watcher.evaluate() } // Dep更新依赖 if (Dep.target) { watcher.depend() } return watcher.value } } }
关键的watcher.js
- get 收集依赖
- udpate 更新dirty为true,从而可以重新evaluate
- evaluate 重新获得computed属性的值
- depend 通知dep收集依赖
- addDep watcher订阅依赖
export default class Watcher {
lazy: boolean;
dirty: boolean;
constructor (
) {
this.dirty = this.lazy // for lazy watchers,dirty用于懒监听
this.value = this.lazy? undefined: this.get() // Dep的target设置为foo watcher
}
// Evaluate the getter, and re-collect dependencies.
get () {
pushTarget(this)
value = this.getter.call(vm, vm)
return value;
}
update () {
if (this.lazy) {
this.dirty = true
}
}
evaluate () {
this.value = this.get()
this.dirty = false
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
addDep (dep: Dep) {
const id = dep.id
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
关键的dep.js
- addSub:添加订阅
- depend: 添加依赖
notify:通知更新
export default class Dep { constructor () { this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null
基于源码分析拆解执行表现
- 初始化a,b,c均为1时,foo()如何计算?
- 如果此时this.a = 0,foo()如何计算?
- 如果此时this.b = 2,foo()如何计算?
如果a的初始值为-1,执行this.a = 1,foo()如何计算?
computed : { foo() { if(this.a>0){ return this.a} else { return this.b + this.c } } } data() { a: 1, b: 1, c: 1, } created(){ this.b = 2; }
初始化a,b,c均为1时,foo()如何计算?
- 初始化watcher
- 创建getter得到value并将dirty置为false
- watcher帮助dep收集依赖,收集的是this.a
- 依赖收集图
初始化watcher
_computedWatchers:{
foo: Watcher {(vm, getter, null, { lazy: true })}
}
// watcher
Watcher: { lazy: true, dirty: true, value: undefined, deps:[] }
创建getter得到value并将dirty置为false
// Watcher: { lazy: true, dirty: true, value: undefined }
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 脏检查, 执行计算
if (watcher.dirty) {
watcher.evaluate() // 得到value,dirty置为false
}
// 返回this.a 1
return watcher.value
}
// watcher.evaluate() 拆解
evaluate () {
// 从foo的getter get()得到value:this.a 1
this.value = this.get()
// 将dirty变为false
this.dirty = false
}
执行完毕后,结果为Watcher { lazy: true, dirty: false, value: this.a, deps: [Dep a]}
watcher帮助dep收集依赖
if (watcher) {
// Dep更新依赖
if (Dep.target) {
watcher.depend()
}
}
// watcher.depend() 拆解
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// dep.depend()拆解
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// watcher.addDep拆解
addDep (dep: Dep) {
const id = dep.id
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
// dep.addSub()拆解
addSub (sub: Watcher) {
this.subs.push(sub)
}
最终结果为:
计算属性foo仅仅收集了this.a作为dep。没有收集b和c。Watcher { lazy: true, dirty: false, value: this.a , deps: [Dep a]}
依赖收集图(dirty为false)
deps: [Dep a(1)]
如果此时this.a = 0,foo()如何计算?
- 执行computedGetter触发watcher.evaluate()
- 在收集了a的基础上,再次收集到b和c的依赖
- 依赖收集图
当我们执行this.a = 0时,a的setter发出依赖更新,getter执行更新,dirty由false变为true。
由于dirty为true,所以执行evaluate,得到foo()的返回值this.b+this.c。
// 发出依赖更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
// dirty由false变为true
update () {
if (this.lazy) {
this.dirty = true
}
}
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 脏检查, 执行计算
if (watcher.dirty) {
// 执行evaluate
watcher.evaluate()
}
// Dep更新依赖
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
evaluate () {
this.value = this.get()
this.dirty = false
}
get () {
pushTarget(this)
value = this.getter.call(vm, vm)
return value;
}
// 收集b和c的依赖
if (Dep.target) {
watcher.depend()
}
最终收集到的依赖为Watcher: { lazy: true, dirty: true, value: this.b+this.c , deps: [Dep a, Dep b, Dep c]}
依赖收集图(dirty为true)
deps: [Dep a(1), Dep b(2), Dep c(2)]
如果此时this.b = 2,foo()如何计算?
- 执行computedGetter不会触发watcher.evaluate()
- 依赖收集图
执行computedGetter不会触发watcher.evaluate()
为什么执行computedGetter不会触发watcher.evaluate()?
因为仅收集了this.a的依赖
当我们执行this.b = 2时,b的setter发出依赖更新,getter执行更新。
但是,由于我们初始化的条件仅仅将this.a作为计算属性foo的依赖,所以不会有任何变化。
// Watcher { lazy: true, dirty: false, value: this.a,deps: [Dep a(1) }
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 此时watcher的dirty为false
if (watcher.dirty) {
watcher.evaluate()
}
// 返回this.a的值 1
return watcher.value
}
}
依赖收集图(dirty为false)
deps: [Dep a(1)]
如果a的初始值为-1,执行this.a = 1,foo()如何计算?
- 每次get都会被收集到依赖中,收集a,b,c依赖到deps
- 由于this.a的依赖被收集到,因此可以直接通过this.a = 0触发更新
- 依赖收集图
源码get()的注释:
Evaluate the getter, and re-collect dependencies.
computed : {
foo() {
// a的get()触发,收集到deps
if(this.a>0){ return this.a}
// b和c的get()触发,收集到deps
else { return this.b + this.c }
}
}
data() {
a: -1,
b: 1,
c: 1,
}
如何收集的?
get () {
pushTarget(this)
value = this.getter.call(vm, vm)
}
此时再触发this.a=1,由于this.a的依赖被收集到,因此可以直接触发更新。
最终返回1。
依赖收集图(dirty为true)
deps: [Dep a(1), Dep a(2), Dep a(2)]
一句话总结
一个computed属性中,每个类似this.foo的调用,都会在get()中重新收集依赖。当依赖收集大于一次(不是一个)时,视为脏(dirty)计算属性,需要重新 evaluate再取值。对于干净的计算属性,不需重新执行evaluate,vue直接取值即可。
期待和大家交流,共同进步:
- 微信公众号: 大大大前端 / excellent_developers
- 前端问答互助星球:t.zsxq.com/yBA2Biq
努力成为优秀前端工程师!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。