vue源碼解析響應(yīng)式原理(computed)

在了解vue computed屬性之前我們首先介紹一下vue的Watcher有:
渲染W(wǎng)atcher,computed Watcher,和usr Watcher 三大類別。其中渲染watcher其實(shí)就是前面文章中mountComponent方法中創(chuàng)建的watcher主要代碼保留主要邏輯如下:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
 //..... 省略相關(guān)邏輯
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

usr Watcher 我們留到下一篇在分析。這里我們主要看看computed watcher,也就是文章的主角computed屬性 。

計(jì)算屬性的初始化是發(fā)生在 Vue 實(shí)例初始化階段的 initState 函數(shù)中,執(zhí)行了 if (opts.computed) initComputed(vm, opts.computed),initComputed 的定義在 src/core/instance/state.js 中:

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      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)
      }
    }
  }
}

函數(shù)首先創(chuàng)建 vm._computedWatchers 為一個(gè)空對象,接著對 computed 對象做遍歷,拿到計(jì)算屬性的每一個(gè) userDef,然后嘗試獲取這個(gè) userDef 對應(yīng)的 getter 函數(shù),拿不到則在開發(fā)環(huán)境下報(bào)警告。接下來為每一個(gè) getter 創(chuàng)建一個(gè) watcher,這個(gè) watcher 和渲染 watcher 有一點(diǎn)很大的不同,它是一個(gè) computed watcher,因?yàn)?const computedWatcherOptions = { computed: true }。最后對判斷如果 key 不是 vm 的屬性,則調(diào)用 defineComputed(vm, key, userDef),否則判斷計(jì)算屬性對于的 key 是否已經(jīng)被 data 或者 prop 所占用,如果是的話則在開發(fā)環(huán)境報(bào)相應(yīng)的警告。

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

這段邏輯很簡單,其實(shí)就是利用 Object.defineProperty 給計(jì)算屬性對應(yīng)的 key 值添加 getter 和 setter,setter 通常是計(jì)算屬性是一個(gè)對象,并且擁有 set 方法的時(shí)候才有,否則是一個(gè)空函數(shù)。在平時(shí)的開發(fā)場景中,計(jì)算屬性有 setter 的情況比較少,我們重點(diǎn)關(guān)注一下 getter 部分,緩存的配置也先忽略,最終 getter 對應(yīng)的是 createComputedGetter(key) 的返回值,來看一下它的定義:

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      watcher.depend()
      return watcher.evaluate()
    }
  }
}

看到這里我們可以看到當(dāng)我們在獲取vm 實(shí)例上的computed 屬性的時(shí)候就會觸發(fā)computedGetter方法。
我們看一下computedWatcher的構(gòu)造函數(shù):

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  // ...
  if (this.computed) {
    this.value = undefined
    this.dep = new Dep()
  } else {
    this.value = this.get()
  }
}  

可以發(fā)現(xiàn) computed watcher 會并不會立刻求值,同時(shí)持有一個(gè) dep 實(shí)例。
然后當(dāng)我們的 render 函數(shù)執(zhí)行訪問到 this.fullName 的時(shí)候,就觸發(fā)了計(jì)算屬性的 getter,它會拿到計(jì)算屬性對應(yīng)的 watcher,然后執(zhí)行 watcher.depend(),來看一下它的定義:

/**
  * Depend on this watcher. Only for computed property watchers.
  */
depend () {
  if (this.dep && Dep.target) {
    this.dep.depend()
  }
}

注意,這時(shí)候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相當(dāng)于渲染 watcher 訂閱了這個(gè) computed watcher 的變化。
然后再執(zhí)行 watcher.evaluate() 去求值,來看一下它的定義:

evaluate () {
  if (this.dirty) {
    this.value = this.get()
    this.dirty = false
  }
  return this.value
}

evaluate 的邏輯非常簡單,判斷 this.dirty,如果為 true 則通過 this.get() 求值,然后把 this.dirty 設(shè)置為 false。在求值過程中,會執(zhí)行 value = this.getter.call(vm, vm),這實(shí)際上就是執(zhí)行了計(jì)算屬性定義的 getter 函數(shù),在我們這個(gè)例子就是執(zhí)行了 return this.firstName + ' ' + this.lastName。
這里需要特別注意的是,由于 this.firstName 和 this.lastName 都是響應(yīng)式對象,這里會觸發(fā)它們的 getter,根據(jù)我們之前的分析,它們會把自身持有的 dep添加到當(dāng)前正在計(jì)算的 watcher 中,這個(gè)時(shí)候 Dep.target 就是這個(gè) computed watcher。
最后通過 return this.value 拿到計(jì)算屬性對應(yīng)的值。我們知道了計(jì)算屬性的求值過程,那么接下來看一下它依賴的數(shù)據(jù)變化后的邏輯。
一旦我們對計(jì)算屬性依賴的數(shù)據(jù)做修改,則會觸發(fā) setter 過程,通知所有訂閱它變化的 watcher 更新,執(zhí)行 watcher.update() 方法:

/* istanbul ignore else */
if (this.computed) {
  // A computed property watcher has two modes: lazy and activated.
  // It initializes as lazy by default, and only becomes activated when
  // it is depended on by at least one subscriber, which is typically
  // another computed property or a component's render function.
  if (this.dep.subs.length === 0) {
    // In lazy mode, we don't want to perform computations until necessary,
    // so we simply mark the watcher as dirty. The actual computation is
    // performed just-in-time in this.evaluate() when the computed property
    // is accessed.
    this.dirty = true
  } else {
    // In activated mode, we want to proactively perform the computation
    // but only notify our subscribers when the value has indeed changed.
    this.getAndInvoke(() => {
      this.dep.notify()
    })
  }
} else if (this.sync) {
  this.run()
} else {
  queueWatcher(this)
}

那么對于計(jì)算屬性這樣的 computed watcher,它實(shí)際上是有 2 種模式,lazy 和 active。如果 this.dep.subs.length === 0 成立,則說明沒有人去訂閱這個(gè) computed watcher 的變化,僅僅把 this.dirty = true,只有當(dāng)下次再訪問這個(gè)計(jì)算屬性的時(shí)候才會重新求值。在this.dep.subs.length>0場景下,表示有渲染 watcher 訂閱了這個(gè) computed watcher 的變化,那么它會執(zhí)行:

this.getAndInvoke(() => {
  this.dep.notify()
})

getAndInvoke (cb: Function) {
  const 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
    const oldValue = this.value
    this.value = value
    this.dirty = false
    if (this.user) {
      try {
        cb.call(this.vm, value, oldValue)
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      cb.call(this.vm, value, oldValue)
    }
  }
}

getAndInvoke 函數(shù)會重新計(jì)算,然后對比新舊值,如果變化了則執(zhí)行回調(diào)函數(shù),那么這里這個(gè)回調(diào)函數(shù)是 this.dep.notify(),在我們這個(gè)場景下就是觸發(fā)了渲染 watcher 重新渲染。
以上就是computed 屬性的源碼解讀。
下一篇我們接著看usr watcher

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容