VUE源碼系列四:計算屬性和監聽屬性,到底該用誰?

前言

上一篇咱們分析了Vue的響應式原理(juejin.im/post/5e0dd4…),今天咱們來搞一下,Vue的計算屬性和監聽屬性的實現原理,以讓咱們更清楚何時該使用computed,何時該使用watch,以及爲何官方不建議使用watch?數組

正文

還記得咱們在data渲染視圖(juejin.im/post/5e06b4…)中講的,New Vue()會發生什麼麼?這其中有一段源代碼:緩存

/*  初始化狀態 */
export function initState (vm: Component) {
  // ...
  /*初始化computed*/
  if (opts.computed) initComputed(vm, opts.computed)
  /*初始化watchers*/
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
複製代碼

能夠看出咱們在new Vue()以後,會執行initState方法,該方法去初始化initComputed(計算)和initWatch(監聽),咱們首先看計算屬性;bash

computed

先看initComputed
源碼:src/core/instance/state.js函數

/* 爲了在屬性值不變的狀況下get()只執行一次而設置的標誌位,下邊會講的 */
const computedWatcherOptions = { lazy: true }
/* 初始化computed */
function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  /* 循環計算屬性,給每一個屬性添加Watcher監聽,不知道Watcher幹什麼的能夠去看https://juejin.im/post/5e0dd467e51d45410f1232f5#heading-13 */
  for (const key in computed) {
    const userDef = computed[key]
    /* 拿get方法 */
    const getter = typeof userDef === 'function' ? userDef : userDef.get
      /* 添加watcher監聽 */
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      /* 若是定義的計算屬性在data或者props中已經被定義過了,會報警告 */
      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)
      }
    }
  }
}
複製代碼

那麼咱們重點看一下defineComputed的實現工具

/**
 * 定義計算屬性
 * @param     {Object | Function}    userDef     計算屬性的值
 */
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  /* 非服務端渲染,執行createComputedGetter */
  const shouldCache = !isServerRendering()
  /* 計算屬性是函數時 */
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
      /* noop是一個空函數,Vue中定義的工具函數 */
    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)
}
複製代碼

能夠看出,本質上就是利用Object.defineProperty去給屬性添加setter和getter,而且不管計算屬性是函數仍是對象,都會去執行createComputedGetter方法,並傳入屬性鍵。oop

function createComputedGetter (key) {
  /* 返回一個函數,即對應的getter */
  return function computedGetter () {
    /* this._computedWatchers是在initComputed方法中定義的一個空對象 */
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      /*****
      Watcher中有evaluate這麼一個方法,當取到get()值之後,將dirty置爲false,那麼下次再去取這個計算屬性值的時候由於dirty已經變爲false了,就不會再去執行get()方法了,而是用的以前的取的值,這就是computed的緩存機制
      evaluate () {
        this.value = this.get()
        this.dirty = false
      }
      ******/
      
      if (watcher.dirty) {
        watcher.evaluate()
      }
      /* 爲了不從新渲染的時候,計算屬性渲染的部分不被從新渲染,所以進行依賴收集 */
      if (Dep.target) {
        watcher.depend()
      }
      /* 返回屬性值 */
      return watcher.value
    }
  }
}
複製代碼

createComputedGetter方法返回一個函數,即對應的是getter方法,該方法主要是返回watcher的值,也就是getter的值,看Watcher的源碼咱們能夠發現dirty的值就是lazy, 而上邊說的const computedWatcherOptions = { lazy: true },lazy初始值爲true,並在上邊initComputed方法中合併給Watcher了,所以計算屬性在屬性值不變的狀況下,只會去執行一次get()方法取值,這也就是爲何Vue的計算屬性有緩存做用。post


咱們舉個例子看一下computed和watch的不一樣,咱們知道computed也會對數據盡心監聽,下邊咱們把計算屬性的監聽暫且叫作computed watcher性能

var vm = new Vue({
  data: {
    firstName: 'yang',
    lastName: 'bo'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})
複製代碼

當初始化fullName時,咱們會執行到Watcher
源碼:/src/core/observer/watcher.jsui

constructor () {
    /* 這一步是給computed watcher設置的,計算屬性並不會去馬上求值 */
    this.value = this.lazy
      ? undefined
      : this.get()
  }
複製代碼

而後當render函數訪問到this.fullName的時候,就會觸發計算屬性的getter,它會拿到計算屬性對應的watcher,而後執行watcher.depend()進行依賴收集。this

depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
}
複製代碼

而後還執行了watcher.evaluate()

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

這個方法咱們上邊已經講了,就不囉嗦了。咱們在看Watcher中的get方法

get () {
    /* 收集Watcher實例,也就是Dep.target */
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    }
    return value
  }
複製代碼

get執行了getter方法,也就是咱們例子中的

this.firstName + ' ' + this.lastName
複製代碼

而後拿到計算屬性最後的value值。

watch

watch初始化也是在initState方法中,上邊已經講到了。

if (opts.watch && opts.watch !== nativeWatch) {
  initWatch(vm, opts.watch)
}
複製代碼

來看一下 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 對象作遍歷,拿到每個 handler,由於 Vue 是支持 watch 的同一個 key 對應多個 handler,因此若是 handler 是一個數組,則遍歷這個數組,調用 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)
}
複製代碼

首先對 hanlder 的類型作判斷,拿到它最終的回調函數,最後調用 vm.$watch(keyOrFn, handler, options) 函數,$watch 是 Vue 原型上的方法,它是在執行 stateMixin 的時候定義的

export function stateMixin (Vue: Class<Component>) {
  // ...
  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 || {}
    /* 用戶自定義watch */
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      }
    }
    /* 返回卸載watcher的方法 */
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
複製代碼

偵聽屬性 watch 最終會調用 $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。最後返回了一個 unwatchFn 方法,它會調用 teardown 方法去移除這個 watcher
因此本質上偵聽屬性也是基於 Watcher 實現的,它是一個 user watcher。其實 Watcher 支持了不一樣的類型,下面咱們梳理一下它有哪些類型以及它們的做用。

Watcher Options

if (options) {
  this.deep = !!options.deep // 深度監聽
  this.user = !!options.user // 在對 watcher 求值以及在執行回調函數的時候,會處理一下錯誤
  this.lazy = !!options.lazy // 惰性求值,賦值給this.dirty,計算屬性的時候用到的
  this.sync = !!options.sync // 在當前 Tick 中同步執行 watcher 的回調函數,不然響應式數據發生變化以後,watcher回調會在nextTick後執行;
} 
複製代碼

因此 watcher 總共有 4 種類型,咱們來一一分析它們,看看不一樣的類型執行的邏輯有哪些差異

deep watcher

也就是咱們一般說的深度監聽,看一下咱們若是將一個對象進行深度監聽會發生什麼:

get () {
  if (this.deep) {
    traverse(value)
  }
  return value
}
複製代碼

看一下traverse源碼:src/core/observer/traverse.js

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 (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)
  }
}
複製代碼

很清晰,對傳入對watch對象進行遞歸遍歷,由於遞歸有必定對性能開銷,所以,咱們必定要在合適的場景去設置deep。

user watcher

就是用戶手寫的watch監聽,前面講過了,略過。

computed watcher

爲計算屬性量身定製的監聽,具備「緩存」功效,前面講過了,略過。

sync watcher

在咱們以前對 setter 的分析過程知道,當響應式數據發送變化後,觸發了 watcher.update(),只是把這個 watcher 推送到一個隊列中,在 nextTick 後纔會真正執行 watcher 的回調函數。而一旦咱們設置了 sync,就能夠在當前 Tick 中同步執行 watcher 的回調函數。

update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      /* 執行Watcher回調,觸發視圖更新 */
      this.run()
    } else {
      queueWatcher(this)
    }
}
複製代碼

所以只有當咱們須要 watch 的值的變化到執行 watcher 的回調函數是一個同步過程的時候纔會去設置該屬性爲 true。

總結

計算屬性和監聽屬性都是經過Watcher這個類去實現當,自己都具備監聽數據的能力。
計算屬性:計算屬性本質上是 computed watcher,計算屬性適合用在模板渲染中,某個值是依賴了其它的響應式對象甚至是計算屬性計算而來,它具備緩存能力,當依賴的值沒有變化甚至是計算結果沒有發生變化,觸發更新的回調則不會執行;
監聽屬性:偵聽屬性本質上是 user watcher,適用於觀測某個值的變化去完成一段複雜的業務邏輯,當新老值相同,也不會去觸發更新回調。

相關文章
相關標籤/搜索