作面試的不倒翁:淺談 Vue 中 computed 實現原理

編者按:咱們會不時邀請工程師談談有意思的技術細節,但願知其因此然能讓你們在面試有更出色表現。 也給面試官提供更多思路。


雖然目前的技術棧已由 Vue 轉到了 React,但從以前使用 Vue 開發的多個項目實際經從來看仍是很是愉悅的,Vue 文檔清晰規範,api 設計簡潔高效,對前端開發人員友好,上手快,甚至我的認爲在不少場景使用 Vue 比 React 開發效率更高,以前也有斷斷續續研讀過 Vue 的源碼,但一直沒有梳理總結,因此在此作一些技術概括同時也加深本身對 Vue 的理解,那麼今天要寫的即是 Vue 中最經常使用到的 API 之一 computed 的實現原理。前端

基本介紹

話很少說,一個最基本的例子以下:vue

<div id="app">
    <p>{{fullName}}</p>
</div>
new Vue({
    data: {
        firstName: 'Xiao',
        lastName: 'Ming'
    },
    computed: {
        fullName: function () {
            return this.firstName + ' ' + this.lastName
        }
    }
})

Vue 中咱們不須要在 template 裏面直接計算 {{this.firstName + ' ' + this.lastName}},由於在模版中放入太多聲明式的邏輯會讓模板自己太重,尤爲當在頁面中使用大量複雜的邏輯表達式處理數據時,會對頁面的可維護性形成很大的影響,而 computed 的設計初衷也正是用於解決此類問題。面試

對比偵聽器 watch

固然不少時候咱們使用 computed 時每每會與 Vue 中另外一個 API 也就是偵聽器 watch 相比較,由於在某些方面它們是一致的,都是以 Vue 的依賴追蹤機制爲基礎,當某個依賴數據發生變化時,全部依賴這個數據的相關數據或函數都會自動發生變化或調用。api

雖然計算屬性在大多數狀況下更合適,但有時也須要一個自定義的偵聽器。這就是爲何 Vue 經過 watch 選項提供了一個更通用的方法來響應數據的變化。當須要在數據變化時執行異步或開銷較大的操做時,這個方式是最有用的。

從 Vue 官方文檔對 watch 的解釋咱們能夠了解到,使用 watch 選項容許咱們執行異步操做(訪問一個 API)或高消耗性能的操做,限制咱們執行該操做的頻率,並在咱們獲得最終結果前,設置中間狀態,而這些都是計算屬性沒法作到的。數組

下面還另外總結了幾點關於 computedwatch 的差別:緩存

  1. computed 是計算一個新的屬性,並將該屬性掛載到 vm(Vue 實例)上,而 watch 是監聽已經存在且已掛載到 vm 上的數據,因此用 watch 一樣能夠監聽 computed 計算屬性的變化(其它還有 dataprops
  2. computed 本質是一個惰性求值的觀察者,具備緩存性,只有當依賴變化後,第一次訪問 computed 屬性,纔會計算新的值,而 watch 則是當數據發生變化便會調用執行函數
  3. 從使用場景上說,computed 適用一個數據被多個數據影響,而 watch 適用一個數據影響多個數據;

以上咱們瞭解了 computedwatch 之間的一些差別和使用場景的區別,固然某些時候二者並無那麼明確嚴格的限制,最後仍是要具體到不一樣的業務進行分析。微信

原理分析

言歸正傳,回到文章的主題 computed 身上,爲了更深層次地瞭解計算屬性的內在機制,接下來就讓咱們一步步探索 Vue 源碼中關於它的實現原理吧。app

在分析 computed 源碼以前咱們先得對 Vue 的響應式系統有一個基本的瞭解,Vue 稱其爲非侵入性的響應式系統,數據模型僅僅是普通的 JavaScript 對象,而當你修改它們時,視圖便會進行自動更新。異步

當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項時,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setter,這些 getter/setter 對用戶來講是不可見的,可是在內部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化,每一個組件實例都有相應的 watcher 實例對象,它會在組件渲染的過程當中把屬性記錄爲依賴,以後當依賴項的 setter 被調用時,會通知 watcher 從新計算,從而導致它關聯的組件得以更新。

Vue 響應系統,其核心有三點:observewatcherdep函數

  1. observe:遍歷 data 中的屬性,使用 Object.definePropertyget/set 方法對其進行數據劫持;
  2. dep:每一個屬性擁有本身的消息訂閱器 dep,用於存放全部訂閱了該屬性的觀察者對象;
  3. watcher:觀察者(對象),經過 dep 實現對響應屬性的監聽,監聽到結果後,主動觸發本身的回調進行響應。

對響應式系統有一個初步瞭解後,咱們再來分析計算屬性。
首先咱們找到計算屬性的初始化是在 src/core/instance/state.js 文件中的 initState 函數中完成的

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // computed初始化
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

調用了 initComputed 函數(其先後也分別初始化了 initDatainitWatch )並傳入兩個參數 vm 實例和 opt.computed 開發者定義的 computed 選項,轉到 initComputed 函數:

const computedWatcherOptions = { computed: true }

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

從這段代碼開始咱們觀察這幾部分:

  1. 獲取計算屬性的定義 userDefgetter 求值函數

    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    定義一個計算屬性有兩種寫法,一種是直接跟一個函數,另外一種是添加 setget 方法的對象形式,因此這裏首先獲取計算屬性的定義 userDef,再根據 userDef 的類型獲取相應的 getter 求值函數。

  2. 計算屬性的觀察者 watcher 和消息訂閱器 dep

    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
    )

    這裏的 watchers 也就是 vm._computedWatchers 對象的引用,存放了每一個計算屬性的觀察者 watcher 實例(注:後文中提到的「計算屬性的觀察者」、「訂閱者」和 watcher 均指代同一個意思但注意和 Watcher 構造函數區分),Watcher 構造函數在實例化時傳入了 4 個參數:vm 實例、getter 求值函數、noop 空函數、computedWatcherOptions 常量對象(在這裏提供給 Watcher 一個標識 {computed:true} 項,代表這是一個計算屬性而不是非計算屬性的觀察者,咱們來到 Watcher 構造函數的定義:

    class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        if (options) {
          this.computed = !!options.computed
        } 
    
        if (this.computed) {
          this.value = undefined
          this.dep = new Dep()
        } else {
          this.value = this.get()
        }
      }
      
      get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          value = this.getter.call(vm, vm)
        } catch (e) {
          
        } finally {
          popTarget()
        }
        return value
      }
      
      update () {
        if (this.computed) {
          if (this.dep.subs.length === 0) {
            this.dirty = true
          } else {
            this.getAndInvoke(() => {
              this.dep.notify()
            })
          }
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
      evaluate () {
        if (this.dirty) {
          this.value = this.get()
          this.dirty = false
        }
        return this.value
      }
    
      depend () {
        if (this.dep && Dep.target) {
          this.dep.depend()
        }
      }
    }

    爲了簡潔突出重點,這裏我手動去掉了咱們暫時不須要關心的代碼片斷。
    觀察 Watcherconstructor ,結合剛纔講到的 new Watcher 傳入的第四個參數 {computed:true} 知道,對於計算屬性而言 watcher 會執行 if 條件成立的代碼 this.dep = new Dep(),而 dep 也就是建立了該屬性的消息訂閱器。

    export default class Dep {
      static target: ?Watcher;
      subs: Array<Watcher>;
    
      constructor () {
        this.id = uid++
        this.subs = []
      }
    
      addSub (sub: Watcher) {
        this.subs.push(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++) {
          subs[i].update()
        }
      }
    }
    
    Dep.target = null

    Dep 一樣精簡了部分代碼,咱們觀察 WatcherDep 的關係,用一句話總結

    watcher 中實例化了 dep 並向 dep.subs 中添加了訂閱者, dep 經過 notify 遍歷了 dep.subs 通知每一個 watcher 更新。
  3. defineComputed 定義計算屬性

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

    由於 computed 屬性是直接掛載到實例對象中的,因此在定義以前須要判斷對象中是否已經存在重名的屬性,defineComputed 傳入了三個參數:vm 實例、計算屬性的 key 以及 userDef 計算屬性的定義(對象或函數)。
    而後繼續找到 defineComputed 定義處:

    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 方法,其中傳入的第三個參數是屬性描述符sharedPropertyDefinition,初始化爲:

    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }

    隨後根據 Object.defineProperty 前面的代碼能夠看到 sharedPropertyDefinitionget/set 方法在通過 userDefshouldCache 等多重判斷後被重寫,當非服務端渲染時,sharedPropertyDefinitionget 函數也就是 createComputedGetter(key) 的結果,咱們找到 createComputedGetter 函數調用結果並最終改寫 sharedPropertyDefinition 大體呈現以下:

    sharedPropertyDefinition = {
        enumerable: true,
        configurable: true,
        get: function computedGetter () {
            const watcher = this._computedWatchers && this._computedWatchers[key]
            if (watcher) {
                watcher.depend()
                return watcher.evaluate()
            }
        },
        set: userDef.set || noop
    }

    當計算屬性被調用時便會執行 get 訪問函數,從而關聯上觀察者對象 watcher 而後執行 wather.depend() 收集依賴和 watcher.evaluate() 計算求值。

分析完全部步驟,咱們再來總結下整個流程:

  1. 當組件初始化的時候,computeddata 會分別創建各自的響應系統,Observer 遍歷 data 中每一個屬性設置 get/set 數據攔截
  2. 初始化 computed 會調用 initComputed 函數

    1. 註冊一個 watcher 實例,並在內實例化一個 Dep 消息訂閱器用做後續收集依賴(好比渲染函數的 watcher 或者其餘觀察該計算屬性變化的 watcher
    2. 調用計算屬性時會觸發其Object.definePropertyget訪問器函數
    3. 調用 watcher.depend() 方法向自身的消息訂閱器 depsubs 中添加其餘屬性的 watcher
    4. 調用 watcherevaluate 方法(進而調用 watcherget 方法)讓自身成爲其餘 watcher 的消息訂閱器的訂閱者,首先將 watcher 賦給 Dep.target,而後執行 getter 求值函數,當訪問求值函數裏面的屬性(好比來自 dataprops 或其餘 computed)時,會一樣觸發它們的 get 訪問器函數從而將該計算屬性的 watcher 添加到求值函數中屬性的 watcher 的消息訂閱器 dep 中,當這些操做完成,最後關閉 Dep.target 賦爲 null 並返回求值函數結果。
  3. 當某個屬性發生變化,觸發 set 攔截函數,而後調用自身消息訂閱器 depnotify 方法,遍歷當前 dep 中保存着全部訂閱者 wathcersubs 數組,並逐個調用 watcherupdate 方法,完成響應更新。

文 / 亦然
一枚嚮往詩與遠方的 coder

編 / 熒聲

本文已由做者受權發佈,版權屬於創宇前端。歡迎註明出處轉載本文。本文連接:https://knownsec-fed.com/2018...

想要訂閱更多來自知道創宇開發一線的分享,請搜索關注咱們的微信公衆號:創宇前端(KnownsecFED)。歡迎留言討論,咱們會盡量回復。

感謝您的閱讀。

相關文章
相關標籤/搜索