vue2.0源碼解讀 - 計(jì)算屬性computed

計(jì)算屬性 VS 偵聽屬性

Vue 的組件對(duì)象支持了計(jì)算屬性 computed 和偵聽屬性 watch 2 個(gè)選項(xiàng)糯耍,很多同學(xué)不了解什么時(shí)候該用 computed 什么時(shí)候該用 watch酥郭。先不回答這個(gè)問題湿弦,我們接下來(lái)從源碼實(shí)現(xiàn)的角度來(lái)分析它們兩者有什么區(qū)別。

computed

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

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null) // 創(chuàng)建空對(duì)象
  // 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 // 是否是函數(shù) 或者有 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. // internal內(nèi)部
      watchers[key] = new Watcher(
        vm,
        getter || noop, // noop 空函數(shù)
        noop, // noop 空函數(shù)
        computedWatcherOptions // lazy: true
      )
    }

    // 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) // userDef = computed[key] 函數(shù)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) { // 計(jì)算屬性是否在 data 或者 props 中存在
        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è)空對(duì)象,接著對(duì) computed 對(duì)象做遍歷掌腰,拿到計(jì)算屬性的每一個(gè) userDef,然后嘗試獲取這個(gè) userDef 對(duì)應(yīng)的 getter 函數(shù)张吉,拿不到則在開發(fā)環(huán)境下報(bào)警告齿梁。接下來(lái)為每一個(gè) getter 創(chuàng)建一個(gè) watcher,這個(gè) watcher渲染 watcher 有一點(diǎn)很大的不同肮蛹,它是一個(gè) computed watcher勺择,因?yàn)?const computedWatcherOptions = { computed: true }。computed watcher 和普通 watcher 的差別我稍后會(huì)介紹蔗崎。最后對(duì)判斷如果 key 不是 vm 的屬性酵幕,則調(diào)用 defineComputed(vm, key, userDef),否則判斷計(jì)算屬性對(duì)于的 key 是否已經(jīng)被 data 或者 prop 所占用缓苛,如果是的話則在開發(fā)環(huán)境報(bào)相應(yīng)的警告芳撒。
那么接下來(lái)需要重點(diǎn)關(guān)注 defineComputed 的實(shí)現(xiàn):

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering() // 服務(wù)端渲染
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache // 上面定義 false
      ? 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)
}

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

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

createComputedGetter 返回一個(gè)函數(shù) computedGetter,它就是計(jì)算屬性對(duì)應(yīng)的 getter爱咬。
整個(gè)計(jì)算屬性的初始化過(guò)程到此結(jié)束尺借,我們知道計(jì)算屬性是一個(gè) computed watcher,它和普通的 watcher 有什么區(qū)別呢精拟,為了更加直觀燎斩,接下來(lái)來(lái)我們來(lái)通過(guò)一個(gè)例子來(lái)分析 computed watcher 的實(shí)現(xiàn)。

var vm = new Vue({
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

當(dāng)初始化這個(gè) computed watcher 實(shí)例的時(shí)候蜂绎,構(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` 會(huì)并不會(huì)立刻求值栅表,同時(shí)持有一個(gè) `dep` 實(shí)例。
然后當(dāng)我們的 `render` 函數(shù)執(zhí)行訪問到 `this.fullName` 的時(shí)候师枣,就觸發(fā)了計(jì)算屬性的 `getter`怪瓶,它會(huì)拿到計(jì)算屬性對(duì)應(yīng)的 `watcher`,然后執(zhí)行 `watcher.depend()`坛吁,來(lái)看一下它的定義:

/** 跟本地代碼不同
  * 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() 去求值,來(lái)看一下它的定義:

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers. // 只為計(jì)算屬性量身打造
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false // 視頻版本有返回 this.value
  }

evaluate 的邏輯非常簡(jiǎn)單宣增,通過(guò) this.get() 求值玫膀,然后把 this.dirty 設(shè)置為 false。在求值過(guò)程中爹脾,會(huì)執(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)式對(duì)象,這里會(huì)觸發(fā)它們的 getter泌霍,根據(jù)我們之前的分析货抄,它們會(huì)把自身持有的 dep添加到當(dāng)前正在計(jì)算的 watcher 中,這個(gè)時(shí)候 Dep.target 就是這個(gè) computed watcher
最后通過(guò) return this.value 拿到計(jì)算屬性對(duì)應(yīng)的值蟹地。我們知道了計(jì)算屬性的求值過(guò)程积暖,那么接下來(lái)看一下它依賴的數(shù)據(jù)變化后的邏輯。
一旦我們對(duì)計(jì)算屬性依賴的數(shù)據(jù)做修改怪与,則會(huì)觸發(fā) setter 過(guò)程夺刑,通知所有訂閱它變化的 watcher 更新,執(zhí)行 watcher.update() 方法:

  /**
   * Subscriber interface.
   * Will be called when a dependency changes. // 
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if ( // 當(dāng)當(dāng)前計(jì)算的 value 和 上一次的value相同時(shí)分别,則什么都不做,否則當(dāng)值一樣時(shí)遍愿,仍然執(zhí)行g(shù)etter,會(huì)重新出發(fā)渲染,造成渲染浪費(fèi)耘斩,計(jì)算成本是較低的错览,而重新渲染成本則較高
        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
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else { // 重新出發(fā)更新
          this.cb.call(this.vm, value, oldValue) // this.cb = this.deps.notify() // 視頻代碼中 callback
        }
      }
    }
  }

函數(shù)會(huì)重新計(jì)算,然后對(duì)比新舊值煌往,如果變化了則執(zhí)行回調(diào)函數(shù)倾哺,那么這里這個(gè)回調(diào)函數(shù)是 this.dep.notify(),在我們這個(gè)場(chǎng)景下就是觸發(fā)了渲染 watcher 重新渲染刽脖。
通過(guò)以上的分析羞海,我們知道計(jì)算屬性本質(zhì)上就是一個(gè) computed watcher,也了解了它的創(chuàng)建過(guò)程和被訪問觸發(fā) getter 以及依賴更新的過(guò)程曲管,其實(shí)這是最新的計(jì)算屬性的實(shí)現(xiàn)却邓,之所以這么設(shè)計(jì)是因?yàn)?Vue 想確保不僅僅是計(jì)算屬性依賴的值發(fā)生變化,而是當(dāng)計(jì)算屬性最終計(jì)算的值發(fā)生變花才會(huì)觸發(fā)渲染 watcher 重新渲染院水,本質(zhì)上是一種優(yōu)化腊徙。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市檬某,隨后出現(xiàn)的幾起案子撬腾,更是在濱河造成了極大的恐慌,老刑警劉巖恢恼,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件民傻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡场斑,警方通過(guò)查閱死者的電腦和手機(jī)漓踢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)漏隐,“玉大人喧半,你說(shuō)我怎么就攤上這事∏嘣穑” “怎么了挺据?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵取具,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我吴菠,道長(zhǎng)者填,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任做葵,我火速辦了婚禮占哟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘酿矢。我一直安慰自己榨乎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布瘫筐。 她就那樣靜靜地躺著蜜暑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪策肝。 梳的紋絲不亂的頭發(fā)上肛捍,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音之众,去河邊找鬼拙毫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛棺禾,可吹牛的內(nèi)容都是我干的缀蹄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼膘婶,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼缺前!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起悬襟,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤衅码,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后古胆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肆良,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年逸绎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片夭谤。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡棺牧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出朗儒,到底是詐尸還是另有隱情颊乘,我是刑警寧澤参淹,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站乏悄,受9級(jí)特大地震影響浙值,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜檩小,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一开呐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧规求,春花似錦筐付、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至丛塌,卻和暖如春较解,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赴邻。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工印衔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人乍楚。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓当编,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親徒溪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子忿偷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 計(jì)算屬性適合用在模版渲染當(dāng)中,某個(gè)值是依賴了其他響應(yīng)式對(duì)象甚至是計(jì)算屬性計(jì)算而來(lái)的臊泌。 偵聽屬性適用在觀測(cè)某個(gè)值的變...
    LoveBugs_King閱讀 1,534評(píng)論 0 0
  • Vue 的組件對(duì)象支持了計(jì)算屬性 computed 和偵聽屬性 watch 2 個(gè)選項(xiàng)鲤桥。我們接下來(lái)從源碼實(shí)現(xiàn)的角度...
    oWSQo閱讀 463評(píng)論 0 0
  • 前言 最近在學(xué)習(xí)Vue計(jì)算屬性的源碼,發(fā)現(xiàn)和普通的響應(yīng)式變量?jī)?nèi)部的實(shí)現(xiàn)還有一些不同渠概,特地寫了這篇博客茶凳,記錄下自己學(xué)...
    心_c2a2閱讀 810評(píng)論 0 3
  • 回憶 watch的過(guò)程就是訂閱數(shù)據(jù),數(shù)據(jù)更新時(shí)執(zhí)行回調(diào)函數(shù)播揪。關(guān)于渲染贮喧,渲染W(wǎng)atcher本身就訂閱了數(shù)據(jù)變化,us...
    LoveBugs_King閱讀 743評(píng)論 0 0
  • 這方面的文章很多猪狈,但是我感覺很多寫的比較抽象箱沦,本文會(huì)通過(guò)舉例更詳細(xì)的解釋。(此文面向的Vue新手們雇庙,如果你是個(gè)大牛...
    Ivy_2016閱讀 15,391評(píng)論 8 64