Vue.js的computed和watch是如何工做的?

文章首發於:github.com/USTB-musion…vue

Vue的組件對象支持計算屬性computed和偵聽屬性watch兩個選項,但這兩個屬性用法有什麼異同以及它們底層實現的原理是什麼?本文將用例子結合源碼來進行總結。node

本文將從如下六個模塊進行總結:ios

  • computed和watch定義
  • computed和watch用法異同
  • watch的高級用法
  • computed的本質 —— computed watch
  • watch底層是如何工做的?
  • 總結

computed和watch定義

1.computed是計算屬性,相似於過濾器,對綁定到視圖的數據進行處理,並監聽變化進而執行對應的方法,對這部分不太明白的話能夠看一下個人另外一篇文章Vue.js的響應式系統原理。官網的例子:git

<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: {
    // 計算屬性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 實例
      return this.message.split('').reverse().join('')
    }
  }
})
複製代碼

結果:github

Original message: "Hello"
Computed reversed message: "olleH"
複製代碼

計算屬性是基於它們的依賴進行緩存的。只在相關依賴發生改變時它們纔會從新求值。值得注意的是「reversedMessage」不能在組件的props和data中定義,不然會報錯。express

2.watch是一個偵聽的動做,用來觀察和響應 Vue 實例上的數據變更。官網上的例子:npm

<div id="watch-example">
  <p>
    Ask a yes/no question:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>
複製代碼
<!-- 由於 AJAX 庫和通用工具的生態已經至關豐富,Vue 核心代碼沒有重複 -->
<!-- 提供這些功能以保持精簡。這也可讓你自由選擇本身更熟悉的工具。 --> <script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script> <script> var watchExampleVM = new Vue({ el: '#watch-example', data: { question: '', answer: 'I cannot give you an answer until you ask a question!' }, watch: { // 若是 `question` 發生改變,這個函數就會運行 question: function (newQuestion, oldQuestion) { this.answer = 'Waiting for you to stop typing...' this.debouncedGetAnswer() } }, created: function () { // `_.debounce` 是一個經過 Lodash 限制操做頻率的函數。 // 在這個例子中,咱們但願限制訪問 yesno.wtf/api 的頻率 // AJAX 請求直到用戶輸入完畢纔會發出。想要了解更多關於 // `_.debounce` 函數 (及其近親 `_.throttle`) 的知識, // 請參考:https://lodash.com/docs#debounce this.debouncedGetAnswer = _.debounce(this.getAnswer, 500) }, methods: { getAnswer: function () { if (this.question.indexOf('?') === -1) { this.answer = 'Questions usually contain a question mark. ;-)' return } this.answer = 'Thinking...' var vm = this axios.get('https://yesno.wtf/api') .then(function (response) { vm.answer = _.capitalize(response.data.answer) }) .catch(function (error) { vm.answer = 'Error! Could not reach the API. ' + error }) } } }) </script> 複製代碼

在這個示例中,使用 watch 選項容許咱們執行異步操做 (訪問一個 API),限制咱們執行該操做的頻率,並在咱們獲得最終結果前,設置中間狀態。這些都是計算屬性沒法作到的。axios

computed和watch用法異同

下面來總結下這二者用法的異同:api

相同: computed和watch都起到監聽/依賴一個數據,並進行處理的做用數組

異同:它們其實都是vue對監聽器的實現,只不過computed主要用於對同步數據的處理,watch則主要用於觀測某個值的變化去完成一段開銷較大的複雜業務邏輯。能用computed的時候優先用computed,避免了多個數據影響其中某個數據時屢次調用watch的尷尬狀況。

watch的高級用法

1.handler方法和immediate屬性

<div id="demo">{{ fullName }}</div>
複製代碼
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      console.log('第一次沒有執行~')
      this.fullName = val + ' ' + this.lastName
    }
  }
})
複製代碼

能夠看到,初始化的時候watch是不會執行的。看上邊的例子,只要當firstName的值改變的時候纔會執行監聽計算。但若是想在第一次它在被綁定的時候就執行怎麼辦?這時候就要修改一下咱們的例子:

watch: {
    firstName: {
      handler(val) {
        console.log('第一次執行了~')
        this.fullName = val + ' ' + this.lastName
      },
      // 表明在watch裏聲明瞭firstName這個方法以後當即先去執行handler方法
      immediate: true
    }
  }
複製代碼

打開控制檯能夠看到打印出了‘第一次執行了~’。注意到handler了嗎,咱們給 firstName 綁定了一個handler方法,以前咱們寫的 watch 方法其實默認寫的就是這個handler,Vue.js會去處理這個邏輯,最終編譯出來其實就是這個handler。

而immediate:true表明若是在 wacth 裏聲明瞭 firstName 以後,就會當即先去執行裏面的handler方法,若是爲 false就跟咱們之前的效果同樣,不會在綁定的時候就執行。爲何加上handler方法和immediate:true就能在綁定的時候第一次就執行呢?待會兒在分析源碼的時候就能理解了。

2.deep屬性

watch裏還有一個deep屬性,表明是否開啓深度監聽,默認爲false,下面來看一個例子:

<div id="app">
  <div>obj.a: {{obj.a}}</div>
  <input type="text" v-model="obj.a">
</div>
複製代碼
var vm = new Vue({
  el: '#app',
  data: {
    obj: {
    	a: 1
    }
  },
  watch: {
    obj: {
      handler(val) {
       console.log('obj.a changed')
      },
      immediate: true
    }
  }
})
複製代碼

當咱們在input輸入框中輸入數據改變obj.a的值時,咱們發如今控制檯沒有打印出'obj.a changed'。受現代 JavaScript 的限制 (以及廢棄 Object.observe),Vue 不能檢測到對象屬性的添加或刪除。因爲 Vue 會在初始化實例時對屬性執行 getter/setter 轉化過程,因此屬性必須在 data 對象上存在才能讓 Vue 轉換它,才能讓它是響應式的。

默認狀況下 在handler方法中 只監聽obj這個屬性它的引用的變化,咱們只有給obj賦值的時候它纔會監聽到,好比咱們在 mounted事件鉤子函數中對obj進行從新賦值:

mounted() {
  this.obj = {
    a: '123'
  }
}
複製代碼

這樣handler就會執行了,且打印出了'obj.a changed'。

可是咱們若是須要監聽obj裏的屬性值呢?這時候,deep屬性就派上用場了。咱們只須要加上deep:true,就能深度監聽obj裏屬性值。

watch: {
    obj: {
      handler(val) {
       console.log('obj.a changed')
      },
      immediate: true,
      deep: true
    }
  }
複製代碼

deep屬性的意思是深度遍歷,會在對象一層層往下遍歷,在每一層都加上監聽器。在源碼中的體現,定義在src/core/observer/traverse.js中:

/* @flow */

import { _Set as Set, isObject } from '../util/index'
import type { SimpleSet } from '../util/index'
import VNode from '../vdom/vnode'

const seenObjects = new Set()

/** * Recursively traverse an object to evoke all converted * getters, so that every nested property inside the object * is collected as a "deep" dependency. */
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

複製代碼

若是this.deep == true,即存在deep,則觸發每一個深層對象的依賴,追蹤其變化。traverse方法遞歸每個對象或者數組,觸發它們的getter,使得對象或數組的每個成員都被依賴收集,造成一個「深(deep)」依賴關係。這個函數實現還有一個小的優化,遍歷過程當中會把子響應式對象經過它們的 dep.id 記錄到 seenObjects,避免之後重複訪問。

可是使用deep屬性會給每一層都加上監聽器,性能開銷可能就會很是大了。這樣咱們能夠用字符串的形式來優化:

watch: {
    'obj.a': {
      handler(val) {
       console.log('obj.a changed')
      },
      immediate: true
      // deep: true
    }
  }
複製代碼

直到遇到'obj.a'屬性,纔會給該屬性設置監聽函數,提升性能。

computed的本質 —— computed watch

咱們知道new Vue()的時候會調用_init方法,該方法會初始化生命週期,初始化事件,初始化render,初始化data,computed,methods,wacther等等。對這部分不太明白的話能夠參考我寫的另一篇文章:Vue.js源碼角度:剖析模版和數據渲染成最終的DOM的過程。今天主要來看如下初始化watch(initWatch)的實現,我加上了註釋方便理解,定義在src/core/instance/state.js中:

// 用於傳入Watcher實例的一個對象,即computed watcher
const computedWatcherOptions = { computed: true }

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  // 聲明一個watchers且同時掛載到vm實例上
  const watchers = vm._computedWatchers = Object.create(null)
  // 在SSR模式下computed屬性只能觸發getter方法
  const isSSR = isServerRendering()

  // 遍歷傳入的computed方法
  for (const key in computed) {
    // 取出computed對象中的每一個方法並賦值給userDef
    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
      )
    }

    // 若是不是SSR服務端渲染,則建立一個watcher實例
    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)) {
      // 若是computed中的key沒有設置到vm中,經過defineComputed函數掛載上去 
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 若是data和props有和computed中的key重名的,會產生warning
      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)
      }
    }
  }
}
複製代碼

經過源碼咱們能夠發現它先聲明瞭一個名爲watchers的空對象,同時在vm上也掛載了這個空對象。以後遍歷計算屬性,並把每一個屬性的方法賦給userDef,若是userDef是function的話就賦給getter,接着判斷是不是服務端渲染,若是不是的話就建立一個Watcher實例。不過須要注意的是,這裏新建的實例中咱們傳入了第四個參數,也就是computedWatcherOptions。const computedWatcherOptions = { computed: true },這個對象是實現computed watcher的關鍵。這時,Watcher中的邏輯就有變化了:

// 源碼定義在src/core/observer/watcher.js中
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    // 其餘的代碼......
    this.dirty = this.computed // for computed watchers
複製代碼

這裏傳入的options就是上邊定義的computedWatcherOptions,當走initData方法的時候,options並不存在,但當走到initComputed的時候,computedWatcherOptions中的computed爲true,注意上邊的一行代碼this.dirty = this.computed,將this.computed賦值給this.dirty。接着看下邊的代碼:

evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }
複製代碼

只有this.dirty爲true的時候才能經過 this.get() 求值,而後把 this.dirty 設置爲 false。在求值過程當中,會執行 value = this.getter.call(vm, vm),這實際上就是執行了計算屬性定義的 getter 函數,不然直接返回value。

當對計算屬性依賴的數據作修改的時候,會觸發 setter 過程,通知全部訂閱它變化的 watcher 更新,執行 watcher.update() 方法:

/** * Subscriber interface. * Will be called when a dependency changes. */
  update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
複製代碼

麼對於計算屬性這樣的 computed watcher,它其實是有 2 種模式,lazy 和 active。若是 this.dep.subs.length === 0 成立,則說明沒有人去訂閱這個 computed watcher 的變化,就把把 this.dirty = true,只有當下次再訪問這個計算屬性的時候纔會從新求值。不然會執行getAndInvoke方法:

getAndInvoke (cb: Function) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }
複製代碼

getAndInvoke 函數會從新計算,而後對比新舊值,在三種狀況下(1.新舊值不想等的狀況2.value是對象或數組的時候3.設置deep屬性的時候)會執行回調函數,那麼這裏這個回調函數是 this.dep.notify(),在咱們這個場景下就是觸發了渲染 watcher 從新渲染。這就能解釋官網中所說的計算屬性是基於它們的依賴進行緩存的

watch底層是如何工做的?

上邊提到了在new Vue()的時候調用了_init方法完成了初始化。在這當中有調用了initWatch方法,定義在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)
    }
  }
}
複製代碼

遍歷watch對象,並將每一個watch[key]賦值給handler,若是是數組則遍歷電影createWatcher方法,不然直接調用createWatcher方法。接下來看一下createWatcher方法的定義:

function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
複製代碼

經過代碼能夠發現,createWatcher方法vm.$$watch(keyOrFn, handler, options) 函數,調用了Vue.prototype.$watch方法,定義在src/core/instance/state.js中:

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 || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
複製代碼

經過代碼咱們能夠發現, watch 最終會調用Vue.prototype.watch 方法,這個方法首先判斷 cb 若是是一個對象,則調用 createWatcher 方法,這是由於watch 方法是用戶能夠直接調用的,它能夠傳遞一個對象,也能夠傳遞函數。接着執行 const watcher = new Watcher(vm, expOrFn, cb, options) 實例化了一個 watcher,這裏須要注意一點這是一個 user watcher,由於 options.user = true。經過實例化 watcher 的方式,一旦咱們 watch 的數據發送變化,它最終會執行 watcher 的 run 方法,執行回調函數 cb,而且若是咱們設置了 immediate 爲 true,則直接會執行回調函數 cb。即設置immediate屬性爲true的時候,第一次watch綁定的時候就能夠執行。最後返回了一個 unwatchFn 方法,它會調用 teardown 方法去移除這個 watcher。

因此watcher是如何工做的?本質上也是基於 Watcher 實現的,它是一個 user watcher。前面提到了計算屬性computed本質上是一個computed watcher。

總結

經過以上的分析,深刻理解了計算屬性computed和偵聽屬性watch是如何工做的。計算屬性本質上是一個computed watch,偵聽屬性本質上是一個user watch。且它們其實都是vue對監聽器的實現,只不過computed主要用於對同步數據的處理,watch則主要用於觀測某個值的變化去完成一段開銷較大的複雜業務邏輯。。能用computed的時候優先用computed,避免了多個數據影響其中某個數據時屢次調用watch的尷尬狀況。

相關文章
相關標籤/搜索