深刻理解 Vue Computed 計算屬性

Computed 計算屬性是 Vue 中經常使用的一個功能,但你理解它是怎麼工做的嗎?html

拿官網簡單的例子來看一下:react

<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 屬性很是頻繁的被使用到,但並非很清楚它的實現原理。好比:計算屬性如何與屬性創建依賴關係?屬性發生變化又如何通知到計算屬性從新計算?express

關於如何創建依賴關係,個人第一個想到的就是語法解析,但這樣太浪費性能,所以排除,第二個想到的就是利用 JavaScript 單線程的原理和 Vue 的 Getter 設計,經過一個簡單的發佈訂閱,就能夠在一次計算屬性求值的過程當中收集到相關依賴。瀏覽器

所以接下來的任務就是從 Vue 源碼一步步分析 Computed 的實現原理。架構

Task

分析依賴收集實現原理,分析動態計算實現原理。框架

Action

data 屬性初始化 getter setter:函數

// src/observer/index.js

// 這裏開始轉換 data 的 getter setter,原始值已存入到 __ob__ 屬性中
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    // 判斷是否處於依賴收集狀態
    if (Dep.target) {
      // 創建依賴關係
      dep.depend()
      ...
    }
    return value
  },
  set: function reactiveSetter (newVal) {
    ...
    // 依賴發生變化,通知到計算屬性從新計算
    dep.notify()
  }
})

computed 計算屬性初始化oop

// src/core/instance/state.js

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

    // 建立屬性 vm.reversedMessage,並將提供的函數將用做屬性 vm.reversedMessage 的 getter,
    // 最終 computed 與 data 會一塊兒混合到 vm 下,因此當 computed 與 data 存在重名屬性時會拋出警告
    defineComputed(vm, key, userDef)
    ...
  }
}

export function defineComputed (target: any, key: string, userDef: Object | Function) {
  ...
  // 建立 get set 方法
  sharedPropertyDefinition.get = createComputedGetter(key)
  sharedPropertyDefinition.set = noop
  ...
  // 建立屬性 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步,判斷是否處於依賴收集狀態
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

不管是屬性仍是計算屬性,都會生成一個對應的 watcher 實例。性能

// src/core/observer/watcher.js

// 當經過 vm.reversedMessage 獲取計算屬性時,就會進到這個 getter 方法
get () {
  // this 指的是 watcher 實例
  // 將當前 watcher 實例暫存到 Dep.target,這就表示開啓了依賴收集任務
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 在執行 vm.reversedMessage 的函調函數時,會觸發屬性(步驟1)和計算屬性(步驟2)的 getter
    // 在這個執行過程當中,就能夠收集到 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)
    }
    // 結束依賴收集任務
    popTarget()
    this.cleanupDeps()
  }
  return value
}

上面多出提到了 dep.depend, dep.notify, Dep.target,那麼 Dep 到底是什麼呢?ui

Dep 的代碼短小精悍,但卻承擔着很是重要的依賴收集環節。

// 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 是給依賴變化時使用的,包含對 watch 的處理
      subs[i].update()
    }
  }
}

// 當首次計算 computed 屬性的值時,Dep 將會在計算期間對依賴進行收集
Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
  // 在一次依賴收集期間,若是有其餘依賴收集任務開始(好比:當前 computed 計算屬性嵌套其餘 computed 計算屬性),
  // 那麼將會把當前 target 暫存到 targetStack,先進行其餘 target 的依賴收集,
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  // 當嵌套的依賴收集任務完成後,將 target 恢復爲上一層的 Watcher,並繼續作依賴收集
  Dep.target = targetStack.pop()
}

Result

總結一下依賴收集、動態計算的流程:

1. data 屬性初始化 getter setter
2. computed 計算屬性初始化,提供的函數將用做屬性 vm.reversedMessage 的 getter
3. 當首次獲取 reversedMessage 計算屬性的值時,Dep 開始依賴收集
4. 在執行 message getter 方法時,若是 Dep 處於依賴收集狀態,則斷定 message 爲 reversedMessage 的依賴,並創建依賴關係
5. 當 message 發生變化時,根據依賴關係,觸發 reverseMessage 的從新計算

到此,整個 Computed 的工做流程就理清楚了。

Vue 是一個設計很是優美的框架,使用 Getter Setter 設計使依賴關係實現的很是順其天然,使用計算與渲染分離的設計(優先使用 MutationObserver,降級使用 setTimeout)也很是貼合瀏覽器計算引擎與排版引擎分離的的設計原理。

若是你想成爲一名架構師,不能只停留在框架的 API 使用層面。

相關文章
相關標籤/搜索