Vue源碼按部就班-Watcher那些事兒

  上一篇數據響應式原理對Vue的實現MVVM的核心思想進行了學習,裏面提到訂閱-發佈模式的訂閱者主要用於響應數據發射變化的更新通知,固然,咱們能夠這麼認爲,Vue中的發佈者其實也有多是訂閱者,能夠訂閱來自其其它組件的更新通知。本文主要對Vue中有哪些Watcher、在何時這些Wathcer會被觸發,以及從源碼角度嘗試總結。javascript

  想一下,咱們須要數據響應的場景? 好比一個購物車功能,看某寶的購物車界面(自動忽略購買內容 ^^):html

購物車圖片

  在購物車方式下單前,咱們須要考慮: 須要選擇哪些來購買,選擇的商品可能買多件,選擇好要購買的商品的時候,咱們要對要花費的RMB進行實時計算,也就是點擊頁面的複選框和修改數量的按鈕都會影響覈算的總消費,這個就能夠利用到Vue裏面的計算屬性了:vue

new Vue({
  name: 'cart',
  data () {
    return {
      selectedCarts: []
    }
  },
  watch: {
    /**
     * 監視selectedCarts變化
     * */
    selectedCarts: {
      handler: function (oldVal, newVal) {
        // do something
      },
      deep: true
    }
  },
  computed: {
    /**
     * 計算總價格
     * @returns {number}
     */
    totalPrice () {
      let totalPrice = 0.0
      this.selectedCarts.forEach((cart) => {
        totalPrice += cart.num * cart.price;
      })
      return totalPrice;
    }
  }
});

  上面示例,就能夠computed裏的總價格totalPrice就能夠根據選中的購物車條目selectedCarts計算得出,在計算出總價格後,會在頁面呈現出計算的結果。此外,咱們能夠經過Vue的watch屬性觀察selectedCarts的變化,根據新舊值比較,能夠下發更新購物車記錄操做(數量)。咱們來看一下這個例子中須要Vue數據作出響應的幾個地方:java

1. 經過computed屬性計算選中的購物車條目的總價格;
2. 經過監視選中的條目下發更新功能;
3. 總價格發生變更時,頁面要及時呈現。

  一、二、3點基本就蘊含Vue中的幾種Watcher: 1.自定義Watcher; 2. Computed屬性(實際上也是Watcher); 3.渲染Watcher(Render Watcher),接下來對這幾種Watcher細細評味。node

1. 自定義Watcher

  自定義Watcher能夠監視的對象包括基本屬性、對象、數組(後兩種都須要指定deep深層次監聽屬性),具體使用能夠看Vue官網watch,好了,知道自定義Wathcer怎麼使用,接下來就看一看Vue內部是怎麼使用的:git

-> vue/src/core/instance/state.js
function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    // 對數組中的每個元素進行監視
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 若是指定的參數爲純對象如:
  // a: {
  //       hander: 'methodName',
  //       deep: Boolean
  //    }
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 若是handler是字符串,則表示方法名,須要根據方法名來獲取到該方法的句柄
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 內部調用$watch
  return vm.$watch(expOrFn, handler, options)
}

// $watch()方法
Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
   // 純對象遞歸調用
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}

    // 用戶自定義Watcher
    options.user = true
    // 建立一個Watcher實例
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // 當即執行回調?
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

  對應watch中的所觀察的數據進行初始化操做,實際上就是爲它們建立一個Watcher實例,固然對數據、對象是要循環、遞歸建立。github

2. Computed屬性

  computed其數據來源是在props或data中定義好的數據(初始化initState時數據能變得可觀察),Vue官網介紹了屬性的用途,主要是解決在template模板中表達式過複雜的問題,都在說computed是基於緩存的,即只有依賴源數據發生改變纔會觸發computed對應數據的計算操做,那麼,咱們應該有好奇它究竟是怎麼個緩存法,續析computed源碼:express

-> 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) {
    // 獲取對應computed屬性的定義 function或者表達式
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    ...

    // 非服務端渲染方式
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions // 定義了屬性: { lazy: true }
      )
    }
    ...
    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    ...
    defineComputed(vm, key, userDef)
    ...
  }
}

  遍歷options中的computed屬性並在非服務器渲染方式的狀況下,依次爲每個計算屬性產生一個Watcher,即computed就是依賴Watcher實現的,但具體和普通的Watcher有什麼不一樣?(後面會進行介紹),繼續看defineComputed實現:api

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 非服務器端渲染,則用緩存
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') { // 函數方式
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop // 空函數
  } else { // 對象方式
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  ...
  // 數據劫持
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

  找到efineComputed中的核心方法createComputedGetter,主要是設置數據劫持操做的getter方法:數組

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) { // dirty標誌數據是否發生變化
        watcher.evaluate()  // 執行watcher.get()方法,並設置dirty爲false 
      }
      if (Dep.target) { // 收集依賴
        watcher.depend()
      }
      return watcher.value
    }
  }
}

  這兒咱們就基本探索到computed屬性計算的核心操做,咱們經過判斷當前watcher(computed)的dirty標誌位判斷是否須要進行重新計算即執行watcher.evaluate內部的watcher.get方法,並設置dirty屬性爲false(主要是在執行get後重置數據爲未更新狀態,便於後續的觀察操做),咱們用購物車示例中的選中的購物車data.selectedCarts數據源結合數據響應式原理講到的數據訂閱-發佈模式來簡單分析一下這個計算過程,給出一個計算流程圖:

computed更新流程示意圖

說明:

1. 更新購物車選中條目or更新條目購買數量    
2. 觸發選中購物車條目selectedCarts的setter進行數據劫持處理    
3. setter通知觀察者notify->update->設置totalPrice對應的Watcher的dirty=true
4. 頁面renderWatcher準備渲染,經過調用totalPriceWatcher的computedGetter的evaluate->get,而後回調totalPrice()方法,計算結果;注意在若是totalPrice依賴的數據源selectedCarts未發生改變時,就會經過computedGetter方法直接返回以前的數據(watcher.value),這也就應證了以前所說的computed是基於緩存的說法。

3. Render Watcher

  組件實例化時會產生一個Watcher,在組件$mount的時候,在mountComponent()中會實例化一個Watcher,並掛載到vm的_watchers上,這個Watcher最終會回調Vue的渲染函數從而完成Vue的更新渲染:

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
 updateComponent = () => {
      // vm._render() 由vm.$options.render()生成的vnode節點
      vm._update(vm._render(), hydrating)
    }

4. 總結

  本文簡要分析了Vue中的Watcher類別,並簡要從源碼角度分析了這三種Watcher的實現,文筆粗淺,不免理解不到位,歡迎指正。另外,歡迎去本人git 相互學習和star,不勝感激。

相關文章
相關標籤/搜索