Vue.js 深入理解 computed 與 watch

前言

vue中computed和watch是vue.js開發者的利器,也是面試必問的題目之一,問題的答案也是可深可淺,可以反應回答者對個這個問題的認識程度(類似于輸入url到頁面渲染發生了哪些事情)


分析

1 用法上的區別:

我的理解是,用到computed往往是我們需要使用他的值(vm[computedKey]),這個值是多個值求值的結果,相當于是我們保存了計算過程,計算過程中使用過的值發生變化時,會觸發重新執行computed[key]函數(或者computed[key].get),例如:

vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

fullName就是我們需要的值,fullName依賴this.firstNamethis.lastName,這兩個依賴值變化時會觸發computed函數重新執行求值。如果該需求使用watch就是這樣子的:

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})

與computed相比,watch實現這種需求顯得很繁瑣。
watch的使用場景如他的名字一樣: 觀察。webpack中可以在config中或者命令行模式中使用watch字段:
webpack.config.js

module.exports = {
  //...
  watch: true
};

命令行

webpack --watch

達到的效果是:當前執行目錄(process.cwd())里面文件發生改變時,webpack能檢測到他變化了,重新打包和熱更新。
vue.js中也是如此,我們觀察某個值變化,這個值變化了,我們來做相應的事情。
例如:
組件的v-model語法糖:

{
    props: {
        value: {
            type: String,
            required: true
        }
    },
    data () {
        return {
            text: ''
        }
    },
    watch: {
        // 父組件中可能改變value綁定的值
        value (val) {
            this.text = val
        },
        text (val) {
            this.$emit('input', val)
        }
    }
}

一句話概括就是: computed[key]這個值我們需要用到它,依賴變化運算過程自動執行,watch[key]這個值我們不需要用到它,它變化了我們想做一些事情。
當然,理論上來說computed能實現上面的需求:

computed: {
   // 這里xxx我們還需要使用到,不然無法觸發求值
    xxx () {
        this.value // 這里啥都不做就是想做個依賴收集
        this.text  // 同上
        // this.text和this.value舊值都需要緩存起來
        if (this.value !== this.value的舊值) {
            this.text = this.value
        }
        if (this.text !== this.text的舊值) {
            this.$emit('input', this.text)
        }
    }
}

這樣實現太繁瑣,所以合適的場景使用合適的api的,這樣才符合設計的初衷。有些場景兩者使用沒有多大區別。

2 源碼分析:

看過源碼的同學都清楚,watch和computed的每一項最終都會執行new Watcher生成一個watcher實例,執行上面會有一些差異。下面開始從源碼分析一下:

注意:vue.js每個版本可能會更改一些邏輯,當前分析版本: v2.6.11 web版,下文中提到的vm[key]相當于我們在vue中使用的this.xxx屬性值

當執行 new Vue({})的時候或者生成組件實例,(組件會類似Copm extend Vue 派生出組件構造類在Vue上),最終都會執行_init()邏輯,如下(這里其他邏輯省略):

_init () {
    ...
    initState(vm)
    ...
}

initState () {
   ...
   initComputed(vm, opts.computed)
   initWatch(vm, opts.watch)
   ...
}

1. watch:

function initWatch (vm, watch) {
    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,expOrFn,handler,options) {
  if (isPlainObject(handler)) { // handler 是否為對象
    // watch[key]可以是函數或者對象
    options = handler
    handler = handler.handler
  }
  //
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

Vue.prototype.$watch = function (expOrFn, cb, options) {
    options = options || {}
    options.user = true
    const watcher = new Wacter(vm, expOrFn,cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
}

 class Watcher {
  constructor (vm, expOrFn, cb, options) {
   // 保留關鍵代碼
    if (options) {
      this.user = !!options.user
      this.lazy = !!options.lazy
    }
    this.cb = cb
    this.active = true
    this.id = ++uid // uid for batching
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 用戶watch邏輯下 expOrFn 為watch[key]的key,類型為 string
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
    }
    get () {
      // 這里做的事情是 Dep.target = this
      pushTarget(this)
      let value
      const vm = this.vm
      // 防止用戶(你)做傻事讓js報錯終止運行
      try {
        // 訪問了 vm.obj.a.b
        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) {
          // deep 對obj的每個obj[key]訪問 觸發依賴收集
          traverse(value)
        }
        // Dep.target = 上一個watcher 實例
        popTarget()
      }
      return value
    }
    addDep (dep) {
      // 一個dep 在一個watcher上只添加一次
      const id = dep.id
      if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
          dep.addSub(this)
        }
      }
    }
    update () {
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) {
        this.run()
      } else {
        queueWatcher(this)
      }
    }
    
    run () {
      if (this.active) {
        const value = this.get()
        if (
          value !== this.value ||
          isObject(value) ||
          this.deep
        ) {
          const oldValue = this.value
          this.value = value
          if (this.user) {
            try {
              this.cb.call(this.vm, value, oldValue)
            }
          } 
        }
      }
    }
  }
  
  function parsePath (path) {
    // 這個函數的目的是返回我們需要觀察的那個值的求值函數
    /*
    我們的定義watch可能是 watch: {
        'obj.a.b.c': {
            handler () {}
        }
    }
    */
    const segments = path.split('.')
    return function (obj) {
      for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
      }
      return obj
    }
}

// 響應式核心代碼
// 一個值有一個dep實例
const dep = new Dep()
Object.defineProperty(obj, key, {
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
  
  class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }
  removeSub (sub) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 先創建的先執行 用戶watcher computed watcher 在渲染watcher之前
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

流程大概是這樣的:

  1. _init -> initWath -> createWatcher(vm, key, handler) -> vm.$watch -> new Watch() -> this.get

  2. this.get()的時候,會執行const value = getter ? getter.call(obj) : valpushTarget(this)(做的事情:Dep.target =this),getter就是對我們要觀測到的值訪問值(比如:'obj.a.b' => obj.a.b),會觸發obj.a.b的get劫持。針對deep的情況會進一步的遞歸訪問值,觸發get劫持。

  3. 執行: dep.depend() -> Dep.target.addDep(dep實例) ->dep.addSub(當前watcher實例),依賴收集完成。


  1. 派發更新邏輯開始, 當obj.a.b的值發生改變時,會觸發set函數,執行dep.notify -> subs[i].update() -> watcher實例.update() -> queueWatcher(push到watcher隊列,排序watcher)->nextTick(flushSchedulerQueue)(下個tick執行watcher隊列的)->watcher.run()
    (后面的代碼分支有點多,就不一一貼上了)

  2. 執行this.get,相當于執行了第二步的,邏輯,比較新舊值是否相等(value基礎類型,引用類型或者deep直接執行接下來的邏輯),執行this.cb.call(this.vm, value, oldValue),this.cb就是用戶定義的watch[key]的函數。所以我們在定義watch函數的時候第一個參數是newValue 第二個參數是oldValue

總結:我們在Vue.js中使用的watch是userWatch,我們觀測某個屬性的變化,監測邏輯和渲染時的依賴收集一樣:dep添加watch,這個值變化了通知所有的watch, 最后會執行我們的定義的watch[key]的handler函數。

提示:<i>渲染watcher類似與上面的watcher,監測的是template中用的值,只要有一個值發生變化,watcher就會觸發,重新渲染。</i>

2. computed:

const computedWatcherOptions = { lazy: true }
function initComputed (vm, computed) {
  for (const key in computed) {
    // 不考慮設置了computed get set 
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (!isSSR) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

class Watcher {
  constructor (vm,expOrFn,cb,options) {
    this.vm = vm
    vm._watchers.push(this)
    if (options) {
      this.lazy = !!options.lazy
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.getter = expOrFn // 用戶computed[key]的值
    // computed 不執行this.get
    this.value = undefined
  }
  get () {
    // Dep.target = 當前watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    }
    // Dep.target設置為上一個watcher 渲染watcher
    popTarget()
    this.cleanupDeps()
    return value
  }
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
  
  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 先創建的先執行 用戶watcher computed watcher 在渲染watcher之前
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}
  
function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get =  createComputedGetter(key)
    sharedPropertyDefinition.set = noop // noop 為空函數
  }
  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()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

大概的流程是:

  1. initComputed遍歷options.computed對象執行new Watcher和defineComputed
  • new Watcher: new Watcher -> 只會執行Watcher類的構造函數

注意:組件這個邏輯會在Vue.extend()(Vue派生的組件構造類)過程中執行,這里分析根節點的

  • defineComputed: sharedPropertyDefinition.get = createComputedGetter(key)(生成vm[key] computed[key] 的get劫持函數),通過object.defineProperty設置vm[key]的get和set,所以能通過this[key]的方式訪問computed的值
  1. 當我們組件中使用到computed[key]即是vm[key]或this[key] (如:前面提到的fullName),觸發createComputedGetter(key)生成的get函數:
  • watcher.evaluate() -> this.get() -> Dep.target = 當前的watcher實例(computed[key]生成的) -> 執行this.getter(用戶定義的computed[key]) -> 觸發函數里面使用this.xxx(如:fullName () {return this.firstName + this.lastName})的get劫持函數和上面的watcher一樣: firstName和lastName都會收集該watcher -> this.dirty = false(項目中多個地方用到了該computed[key],watcher.evaluate()只需要執行一次)
  • watcher.depend() -> watcher.dep[i].depend() computed[key]使用到的一個值(firstName lastName)就擁有一個dep,(deps包含了firstName和lastName的dep)->
    Dep.target.addDep(this) 上一步的時候Dep.target已經設置為上一個watcher了,即是渲染watcher ->
    這些dep也會收集渲染watcher
  • return watcher.value computed[key] (vm[key]或this[key])就是watcher.value
  1. 當computed 依賴的這些值(fistName或者lastName發生變化)發生變化時,觸發set邏輯 -> dep.notify 通過id排序 確保computed watcher先執行,dep訂閱的watcher遍歷執行update -> this.dirty = true -> 執行渲染watcher.update -> 組件template重新渲染 -> 再執行第2步保持值為最新值。

注意:這個版本好像computed沒有之前所謂的緩存,newVal oldVal不會比較了,依賴的值發生改變,重新求值。

而且vue的官方文檔中也提到:

不同的是計算屬性是基于它們的響應式依賴進行緩存的。只在相關響應式依賴發生改變時它們才會重新求值。這就意味著只要 message 還沒有發生改變,多次訪問 reversedMessage 計算屬性會立即返回之前的計算結果,而不必再次執行函數。

之前的版本我記得是這樣的, c () {return this.a + this.b},a = 1, b =2 -> a =2, b=1,最終的值不變就會緩存,現在不再說computed會比較新舊值了,而是說明依賴發生改變,computed就重新求值。

總結:

  1. watcher和dep相互收集,我們定義的data中的一個屬性(基礎類型,引用類型遞歸創建dep, 確保一個基礎類型一個dep)擁有一個dep,dep會收集所有的watcher(渲染watcher 、computed watcher 、用戶定義的watcher), watcher也會記錄被哪些dep收集了,當然這個過程中會有一個去重處理, data.xxx發生變化會通知所有的watcher, dep是obj.xxx和watcher的一個橋梁。
  2. watch(用戶watcher)和computed的異同點:
  • 相同點:computed[key]和user watcher都會生成一個watcher實例。

  • 不同點

    1. dep不同:watch監聽的是vm[key]的變化,vm[key]的dep, vm[key]變化觸發watch.get求值,觸發watcher回調函數。computed中的dep是computed[key]執行過程中訪問的dep,即用到了哪些值。
    2. dep收集的對象不同: 執行用戶watcher.get的時候,watch的[key] (vm[key])的dep只會收集當然watcher,computed watcher中的dep會收集渲染watcher和computed watcher
    3. 執行時機不同: watcher immediate除外,computed[key]在我們使用到時會觸發getter,觸發watcher.get()執行computed[key], watch則只會在vm[key]改變觸發this.cb即watch中定義的handler。

最后:

  • 如果有錯誤歡迎指出
  • 如有幫助歡迎點贊_
  • vue源碼分析附上我總結的思維導圖


    Vue源碼分析.png
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容