FrankKai / FrankKai.github.io

FE blog
https://frankkai.github.io/
362 stars 39 forks source link

如何理解vue的computed? #213

Open FrankKai opened 4 years ago

FrankKai commented 4 years ago

这道考察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呢?

目录

FrankKai commented 4 years ago

执行表现

如果此时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?

FrankKai commented 4 years ago

源码分析

对于this.b = 2,vue跳过evaluate阶段,直接得到返回值this.a,是如何优化的呢? 下面我们来看源码: 源码地址:state.js computed相关的有三个非常重要的函数:

先来看看最最核心的代码

// 脏检查, 执行计算
 if (watcher.dirty) {
    watcher.evaluate()
  }
 // Dep更新依赖
 if (Dep.target) {
    watcher.depend()
 }

下面再看具体的源码

createComputedGetter

关键的watcher.js

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

FrankKai commented 4 years ago

基于源码分析拆解执行表现

初始化a,b,c均为1时,foo()如何计算?

初始化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)] image

如果此时this.a = 0,foo()如何计算?

当我们执行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)] image

如果此时this.b = 2,foo()如何计算?

执行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)] image

如果a的初始值为-1,执行this.a = 1,foo()如何计算?

源码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)] image

FrankKai commented 4 years ago

一句话总结

一个computed属性中,每个类似this.foo的调用,都会在get()中重新收集依赖。 当依赖收集大于一次(不是一个)时,视为脏(dirty)计算属性,需要重新 evaluate再取值。 对于干净的计算属性,不需重新执行evaluate,vue直接取值即可。