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

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

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
 //..... 省略相關邏輯
  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屬性 。

計算屬性的初始化是發(fā)生在 Vue 實例初始化階段的 initState 函數中键菱,執(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)
      }
    }
  }
}

函數首先創(chuàng)建 vm._computedWatchers 為一個空對象,接著對 computed 對象做遍歷,拿到計算屬性的每一個 userDef芭梯,然后嘗試獲取這個 userDef 對應的 getter 函數险耀,拿不到則在開發(fā)環(huán)境下報警告。接下來為每一個 getter 創(chuàng)建一個 watcher玖喘,這個 watcher 和渲染 watcher 有一點很大的不同,它是一個 computed watcher蘑志,因為 const computedWatcherOptions = { computed: true }累奈。最后對判斷如果 key 不是 vm 的屬性,則調用 defineComputed(vm, key, userDef)急但,否則判斷計算屬性對于的 key 是否已經被 data 或者 prop 所占用澎媒,如果是的話則在開發(fā)環(huán)境報相應的警告。

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)
}

這段邏輯很簡單波桩,其實就是利用 Object.defineProperty 給計算屬性對應的 key 值添加 getter 和 setter戒努,setter 通常是計算屬性是一個對象,并且擁有 set 方法的時候才有镐躲,否則是一個空函數储玫。在平時的開發(fā)場景中,計算屬性有 setter 的情況比較少萤皂,我們重點關注一下 getter 部分撒穷,緩存的配置也先忽略,最終 getter 對應的是 createComputedGetter(key) 的返回值裆熙,來看一下它的定義:

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

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

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ā)現 computed watcher 會并不會立刻求值,同時持有一個 dep 實例入录。
然后當我們的 render 函數執(zhí)行訪問到 this.fullName 的時候蛤奥,就觸發(fā)了計算屬性的 getter,它會拿到計算屬性對應的 watcher僚稿,然后執(zhí)行 watcher.depend()凡桥,來看一下它的定義:

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

注意,這時候的 Dep.target 是渲染 watcher贫奠,所以 this.dep.depend() 相當于渲染 watcher 訂閱了這個 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 設置為 false谢肾。在求值過程中腕侄,會執(zhí)行 value = this.getter.call(vm, vm),這實際上就是執(zhí)行了計算屬性定義的 getter 函數,在我們這個例子就是執(zhí)行了 return this.firstName + ' ' + this.lastName冕杠。
這里需要特別注意的是微姊,由于 this.firstName 和 this.lastName 都是響應式對象,這里會觸發(fā)它們的 getter分预,根據我們之前的分析兢交,它們會把自身持有的 dep添加到當前正在計算的 watcher 中,這個時候 Dep.target 就是這個 computed watcher笼痹。
最后通過 return this.value 拿到計算屬性對應的值配喳。我們知道了計算屬性的求值過程,那么接下來看一下它依賴的數據變化后的邏輯凳干。
一旦我們對計算屬性依賴的數據做修改晴裹,則會觸發(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)
}

那么對于計算屬性這樣的 computed watcher涧团,它實際上是有 2 種模式,lazy 和 active经磅。如果 this.dep.subs.length === 0 成立泌绣,則說明沒有人去訂閱這個 computed watcher 的變化,僅僅把 this.dirty = true馋贤,只有當下次再訪問這個計算屬性的時候才會重新求值赞别。在this.dep.subs.length>0場景下,表示有渲染 watcher 訂閱了這個 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 函數會重新計算仿滔,然后對比新舊值,如果變化了則執(zhí)行回調函數犹芹,那么這里這個回調函數是 this.dep.notify()崎页,在我們這個場景下就是觸發(fā)了渲染 watcher 重新渲染。
以上就是computed 屬性的源碼解讀腰埂。
下一篇我們接著看usr watcher

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末飒焦,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子屿笼,更是在濱河造成了極大的恐慌牺荠,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驴一,死亡現場離奇詭異休雌,居然都是意外死亡,警方通過查閱死者的電腦和手機肝断,發(fā)現死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進店門杈曲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來驰凛,“玉大人,你說我怎么就攤上這事担扑∏∠欤” “怎么了?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵涌献,是天一觀的道長胚宦。 經常有香客問我,道長洁奈,這世上最難降的妖魔是什么间唉? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮利术,結果婚禮上,老公的妹妹穿的比我還像新娘低矮。我一直安慰自己印叁,他們只是感情好,可當我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布军掂。 她就那樣靜靜地躺著轮蜕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蝗锥。 梳的紋絲不亂的頭發(fā)上跃洛,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天,我揣著相機與錄音终议,去河邊找鬼汇竭。 笑死,一個胖子當著我的面吹牛穴张,可吹牛的內容都是我干的细燎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼皂甘,長吁一口氣:“原來是場噩夢啊……” “哼玻驻!你這毒婦竟也來了?” 一聲冷哼從身側響起偿枕,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤璧瞬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后渐夸,有當地人在樹林里發(fā)現了一具尸體嗤锉,經...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年捺萌,在試婚紗的時候發(fā)現自己被綠了档冬。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膘茎。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖酷誓,靈堂內的尸體忽然破棺而出披坏,到底是詐尸還是另有隱情,我是刑警寧澤盐数,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布棒拂,位于F島的核電站,受9級特大地震影響玫氢,放射性物質發(fā)生泄漏帚屉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一漾峡、第九天 我趴在偏房一處隱蔽的房頂上張望攻旦。 院中可真熱鬧,春花似錦生逸、人聲如沸牢屋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烙无。三九已至,卻和暖如春遍尺,著一層夾襖步出監(jiān)牢的瞬間截酷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工乾戏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留迂苛,地道東北人。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓歧蕉,卻偏偏與公主長得像灾部,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子惯退,可洞房花燭夜當晚...
    茶點故事閱讀 44,969評論 2 355