淺談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的設計初衷也正是用於解決此類問題。api

對比偵聽器watch

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

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

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

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

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

原理分析

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

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

clipboard.png

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

Vue響應系統,其核心有三點:observewatcherdepoop

  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


分析完以上步驟,咱們再來梳理下整個流程:

  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方法,完成響應更新。
相關文章
相關標籤/搜索