深入理解 Vue Computed 計(jì)算屬性

Computed 計(jì)算屬性是 Vue 中常用的一個(gè)功能,但你理解它是怎么工作的嗎?

拿官網(wǎng)簡(jiǎn)單的例子來(lái)看一下:

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // a computed getter
    reversedMessage: function () {
      // `this` points to the vm instance
      return this.message.split('').reverse().join('')
    }
  }
})

Situation

Vue 里的 Computed 屬性非常頻繁的被使用到,但并不是很清楚它的實(shí)現(xiàn)原理。比如:計(jì)算屬性如何與屬性建立依賴關(guān)系囚玫?屬性發(fā)生變化又如何通知到計(jì)算屬性重新計(jì)算?

關(guān)于如何建立依賴關(guān)系读规,我的第一個(gè)想到的就是語(yǔ)法解析抓督,但這樣太浪費(fèi)性能,因此排除束亏,第二個(gè)想到的就是利用 JavaScript 單線程的原理和 Vue 的 Getter 設(shè)計(jì)铃在,通過(guò)一個(gè)簡(jiǎn)單的發(fā)布訂閱,就可以在一次計(jì)算屬性求值的過(guò)程中收集到相關(guān)依賴碍遍。

因此接下來(lái)的任務(wù)就是從 Vue 源碼一步步分析 Computed 的實(shí)現(xiàn)原理定铜。

Task

分析依賴收集實(shí)現(xiàn)原理,分析動(dòng)態(tài)計(jì)算實(shí)現(xiàn)原理怕敬。

Action

data 屬性初始化 getter setter:

// src/observer/index.js

// 這里開(kāi)始轉(zhuǎn)換 data 的 getter setter揣炕,原始值已存入到 __ob__ 屬性中
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    // 判斷是否處于依賴收集狀態(tài)
    if (Dep.target) {
      // 建立依賴關(guān)系
      dep.depend()
      ...
    }
    return value
  },
  set: function reactiveSetter (newVal) {
    ...
    // 依賴發(fā)生變化,通知到計(jì)算屬性重新計(jì)算
    dep.notify()
  }
})

computed 計(jì)算屬性初始化

// src/core/instance/state.js

// 初始化計(jì)算屬性
function initComputed (vm: Component, computed: Object) {
  ...
  // 遍歷 computed 計(jì)算屬性
  for (const key in computed) {
    ...
    // 創(chuàng)建 Watcher 實(shí)例
    // create internal watcher for the computed property.
    watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)

    // 創(chuàng)建屬性 vm.reversedMessage东跪,并將提供的函數(shù)將用作屬性 vm.reversedMessage 的 getter畸陡,
    // 最終 computed 與 data 會(huì)一起混合到 vm 下,所以當(dāng) computed 與 data 存在重名屬性時(shí)會(huì)拋出警告
    defineComputed(vm, key, userDef)
    ...
  }
}

export function defineComputed (target: any, key: string, userDef: Object | Function) {
  ...
  // 創(chuàng)建 get set 方法
  sharedPropertyDefinition.get = createComputedGetter(key)
  sharedPropertyDefinition.set = noop
  ...
  // 創(chuàng)建屬性 vm.reversedMessage虽填,并初始化 getter setter
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        // watcher 暴露 evaluate 方法用于取值操作
        watcher.evaluate()
      }
      // 同第1步丁恭,判斷是否處于依賴收集狀態(tài)
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

無(wú)論是屬性還是計(jì)算屬性,都會(huì)生成一個(gè)對(duì)應(yīng)的 watcher 實(shí)例斋日。

// src/core/observer/watcher.js

// 當(dāng)通過(guò) vm.reversedMessage 獲取計(jì)算屬性時(shí)牲览,就會(huì)進(jìn)到這個(gè) getter 方法
get () {
  // this 指的是 watcher 實(shí)例
  // 將當(dāng)前 watcher 實(shí)例暫存到 Dep.target,這就表示開(kāi)啟了依賴收集任務(wù)
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 在執(zhí)行 vm.reversedMessage 的函調(diào)函數(shù)時(shí)恶守,會(huì)觸發(fā)屬性(步驟1)和計(jì)算屬性(步驟2)的 getter
    // 在這個(gè)執(zhí)行過(guò)程中竭恬,就可以收集到 vm.reversedMessage 的依賴了
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    if (this.deep) {
      traverse(value)
    }
    // 結(jié)束依賴收集任務(wù)
    popTarget()
    this.cleanupDeps()
  }
  return value
}

上面多出提到了 dep.depend, dep.notify, Dep.target跛蛋,那么 Dep 究竟是什么呢?

Dep 的代碼短小精悍痊硕,但卻承擔(dān)著非常重要的依賴收集環(huán)節(jié)。

// src/core/observer/dep.js

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, 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++) {
      // 更新 watcher 的值押框,與 watcher.evaluate() 類似岔绸,
      // 但 update 是給依賴變化時(shí)使用的,包含對(duì) watch 的處理
      subs[i].update()
    }
  }
}

// 當(dāng)首次計(jì)算 computed 屬性的值時(shí)橡伞,Dep 將會(huì)在計(jì)算期間對(duì)依賴進(jìn)行收集
Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
  // 在一次依賴收集期間盒揉,如果有其他依賴收集任務(wù)開(kāi)始(比如:當(dāng)前 computed 計(jì)算屬性嵌套其他 computed 計(jì)算屬性),
  // 那么將會(huì)把當(dāng)前 target 暫存到 targetStack兑徘,先進(jìn)行其他 target 的依賴收集刚盈,
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  // 當(dāng)嵌套的依賴收集任務(wù)完成后,將 target 恢復(fù)為上一層的 Watcher挂脑,并繼續(xù)做依賴收集
  Dep.target = targetStack.pop()
}

Result

總結(jié)一下依賴收集藕漱、動(dòng)態(tài)計(jì)算的流程:

1. data 屬性初始化 getter setter
2. computed 計(jì)算屬性初始化,提供的函數(shù)將用作屬性 vm.reversedMessage 的 getter
3. 當(dāng)首次獲取 reversedMessage 計(jì)算屬性的值時(shí)崭闲,Dep 開(kāi)始依賴收集
4. 在執(zhí)行 message getter 方法時(shí)肋联,如果 Dep 處于依賴收集狀態(tài),則判定 message 為 reversedMessage 的依賴刁俭,并建立依賴關(guān)系
5. 當(dāng) message 發(fā)生變化時(shí)橄仍,根據(jù)依賴關(guān)系,觸發(fā) reverseMessage 的重新計(jì)算

到此牍戚,整個(gè) Computed 的工作流程就理清楚了侮繁。

Vue 是一個(gè)設(shè)計(jì)非常優(yōu)美的框架,使用 Getter Setter 設(shè)計(jì)使依賴關(guān)系實(shí)現(xiàn)的非常順其自然如孝,使用計(jì)算與渲染分離的設(shè)計(jì)(優(yōu)先使用 MutationObserver宪哩,降級(jí)使用 setTimeout)也非常貼合瀏覽器計(jì)算引擎與排版引擎分離的的設(shè)計(jì)原理。

如果你想成為一名架構(gòu)師暑竟,不能只停留在框架的 API 使用層面斋射。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市但荤,隨后出現(xiàn)的幾起案子罗岖,更是在濱河造成了極大的恐慌,老刑警劉巖腹躁,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桑包,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡纺非,警方通過(guò)查閱死者的電腦和手機(jī)哑了,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門赘方,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人弱左,你說(shuō)我怎么就攤上這事窄陡。” “怎么了拆火?”我有些...
    開(kāi)封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵跳夭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我们镜,道長(zhǎng)币叹,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任模狭,我火速辦了婚禮颈抚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嚼鹉。我一直安慰自己贩汉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布反砌。 她就那樣靜靜地躺著雾鬼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宴树。 梳的紋絲不亂的頭發(fā)上策菜,一...
    開(kāi)封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音酒贬,去河邊找鬼又憨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛锭吨,可吹牛的內(nèi)容都是我干的蠢莺。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼零如,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼躏将!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起考蕾,我...
    開(kāi)封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤祸憋,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后肖卧,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蚯窥,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拦赠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片巍沙。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖荷鼠,靈堂內(nèi)的尸體忽然破棺而出句携,到底是詐尸還是另有隱情,我是刑警寧澤允乐,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布务甥,位于F島的核電站,受9級(jí)特大地震影響喳篇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜态辛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一麸澜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧奏黑,春花似錦炊邦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蹂匹,卻和暖如春碘菜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背限寞。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工忍啸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人履植。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓计雌,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親玫霎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子凿滤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

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

  • 這方面的文章很多,但是我感覺(jué)很多寫的比較抽象庶近,本文會(huì)通過(guò)舉例更詳細(xì)的解釋翁脆。(此文面向的Vue新手們,如果你是個(gè)大牛...
    Ivy_2016閱讀 15,391評(píng)論 8 64
  • Vue 依賴收集原理分析 Vue實(shí)例在初始化時(shí)拦盹,可以接受以下幾類數(shù)據(jù): 模板 初始化數(shù)據(jù) 傳遞給組件的屬性值 co...
    wuww閱讀 6,872評(píng)論 3 19
  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內(nèi)容鹃祖,還有我對(duì)于 Vue 1.0 印象不深的內(nèi)容。關(guān)于...
    云之外閱讀 5,050評(píng)論 0 29
  • 吃了兩口小蛋糕,翻閱完機(jī)上的雜誌恬口,兩個(gè)小時(shí)的航程校读,我有些困倦了,當(dāng)有些許睡意襲來(lái)祖能,我伸手去關(guān)閉刺眼的遮陽(yáng)板歉秫,想小睡...
    林素兮閱讀 595評(píng)論 22 4
  • 李洪鋒 周亞夫者,先漢絳侯周勃之次子也养铸。 亞夫出身豪門雁芙,算是皇親國(guó)戚,他的嫂子就是文帝的女兒钞螟。不過(guò)兔甘,他并非因?yàn)檫@個(gè)...
    天馬一號(hào)閱讀 1,099評(píng)論 2 5