1. 程式人生 > 程式設計 >詳解Vue中的watch和computed

詳解Vue中的watch和computed

前言

對於使用Vue的前端而言,watch、computed和methods三個屬性相信是不陌生的,是日常開發中經常使用的屬性。但是對於它們的區別及使用場景,又是否清楚,本文我將跟大家一起通過原始碼來分析這三者的背後實現原理,更進一步地理解它們所代表的含義。 在繼續閱讀本文之前,希望你已經具備了一定的Vue使用經驗,如果想學習Vue相關知識,請移步至官網。

Watch

我們先來找到watch的初始化的程式碼,/src/core/instance/state.js

export function initState (vm: Component) {
 vm._watchers = []
 const opts = vm.$options
 if (opts.props) initProps(vm,opts.props) // 初始化props
 if (opts.methods) initMethods(vm,opts.methods) // 初始化方法
 if (opts.data) {
 initData(vm) // 先初始化data 重點
 } else {
 observe(vm._data = {},true /* asRootData */)
 }
 if (opts.computed) initComputed(vm,opts.computed) // 初始化computed
 if (opts.watch && opts.watch !== nativeWatch) {
 initWatch(vm,opts.watch) // 初始化watch
 }
}

接下來我們深入分析一下initWatch的作用,不過在接下去之前,這裡有一點是data的初始化是在computed和watch初始化之前,這是為什麼呢?大家可以停在這裡想一下這個問題。想不通也沒關係,繼續接下來的原始碼分析,這個問題也會迎刃而解。

initWatch

function initWatch (vm: Component,watch: Object) {
 for (const key in watch) {
 const handler = watch[key]
 if (Array.isArray(handler)) { // 如果handler是一個數組
  for (let i = 0; i < handler.length; i++) { // 遍歷watch的每一項,執行createWatcher
  createWatcher(vm,key,handler[i])
  }
 } else {
  createWatcher(vm,handler) 
 }
 }
}

createWatcher

function createWatcher (
 vm: Component,expOrFn: string | Function,handler: any,options?: Object
) {
 if (isPlainObject(handler)) { // 判斷handler是否是純物件,對options和handler重新賦值
 options = handler
 handler = handler.handler
 }
 if (typeof handler === 'string') { // handler用的是methods上面的方法,具體用法請檢視官網文件
 handler = vm[handler]
 }
 // expOrnFn: watch的key值, handler: 回撥函式 options: 可選配置
 return vm.$watch(expOrFn,handler,options) // 呼叫原型上的$watch
}

Vue.prototype.$watch

 Vue.prototype.$watch = function (
 expOrFn: string | Function,cb: any,options?: Object
 ): Function {
 const vm: Component = this
 if (isPlainObject(cb)) { // 判斷cb是否是物件,如果是則繼續呼叫createWatcher
  return createWatcher(vm,expOrFn,cb,options)
 }
 options = options || {}
 options.user = true // user Watcher的標示 options = { user: true, ...options }
 const watcher = new Watcher(vm,options) // new Watcher 生成一個user Watcher
 if (options.immediate) { // 如果傳入了immediate 則直接執行回撥cb
  try {
  cb.call(vm,watcher.value)
  } catch (error) {
  handleError(error,vm,`callback for immediate watcher "${watcher.expression}"`)
  }
 }
 return function unwatchFn () {
  watcher.teardown()
 }
 }
}

上面幾個函式呼叫的邏輯都比較簡單,所以就在程式碼上寫了註釋。我們重點關注一下這個userWatcher生成的時候做了什麼。

Watcher

又來到了我們比較常見的Watcher類的階段了,這次我們重點關注生成userWatch的過程。

export default class Watcher {
 vm: Component;
 expression: string;
 cb: Function;
 id: number;
 deep: boolean;
 user: boolean;
 lazy: boolean;
 sync: boolean;
 dirty: boolean;
 active: boolean;
 deps: Array<Dep>;
 newDeps: Array<Dep>;
 depIds: SimpleSet;
 newDepIds: SimpleSet;
 before: ?Function;
 getter: Function;
 value: any;

 constructor (
 vm: Component,cb: Function,options?: ?Object,isRenderWatcher?: boolean
 ) {
 this.vm = vm
 if (isRenderWatcher) {
  vm._watcher = this
 }
 vm._watchers.push(this)
 // options
 if (options) { // 在 new UserWatcher的時候傳入了options,並且options.user = true
  this.deep = !!options.deep
  this.user = !!options.user
  this.lazy = !!options.lazy
  this.sync = !!options.sync
  this.before = options.before
 } else {
  this.deep = this.user = this.lazy = this.sync = false
 }
 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.expression = process.env.NODE_ENV !== 'production' // 一個函式表示式
  ? expOrFn.toString()
  : ''
 // parse expression for getter
 if (typeof expOrFn === 'function') { 
  this.getter = expOrFn
 } else {
  this.getter = parsePath(expOrFn) // 進入這個邏輯,呼叫parsePath方法,對getter進行賦值
  if (!this.getter) {
  this.getter = noop
  process.env.NODE_ENV !== 'production' && warn(
   `Failed watching path: "${expOrFn}" ` +
   'Watcher only accepts simple dot-delimited paths. ' +
   'For full control,use a function instead.',vm
  )
  }
 }
 this.value = this.lazy
  ? undefined
  : this.get()
 }
}

首先會對這個watcher的屬性進行一系列的初始化配置,接著判斷expOrFn這個值,對於我們watch的key而言,不是函式所以會執行parsePath函式,該函式定義如下:

/**
 * Parse simple path.
 */
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
 if (bailRE.test(path)) {
 return
 }
 const segments = path.split('.')
 return function (obj) {
 for (let i = 0; i < segments.length; i++) { // 遍歷陣列
  if (!obj) return
  obj = obj[segments[i]] // 每次把當前的key值對應的值重新賦值obj
 }
 return obj
 }
}

首先會判斷傳入的path是否符合預期,如果不符合則直接return,接著講path根據'.'字串進行拆分,因為我們傳入的watch可能有如下幾種形式:

watch: {
	a: () {}
 'formData.a': () {}
}

所以需要對path進行拆分,接下來遍歷拆分後的陣列,這裡返回的函式的引數obj其實就是vm例項,通過vm[segments[i]],就可以最終找到這個watch所對應的屬性,最後將obj返回。

constructor () { // 初始化的最後一段邏輯
	this.value = this.lazy // 因為this.lazy為false,所以會執行this.get方法
  ? undefined
  : this.get()
}
  
get () {
 pushTarget(this) // 將當前的watcher例項賦值給 Dep.target
 let value
 const vm = this.vm
 try {
  value = this.getter.call(vm,vm) // 這裡的getter就是上文所講parsePath放回的函式,並將vm例項當做第一個引數傳入
 } catch (e) {
  if (this.user) {
  handleError(e,`getter for watcher "${this.expression}"`) // 如果報錯了會這這一塊邏輯
  } else {
  throw e
  }
 } finally {
  // "touch" every property so they are all tracked as
  // dependencies for deep watching
  if (this.deep) { // 如果deep為true,則執行深遞迴
  traverse(value)
  }
  popTarget() // 將當前watch出棧
  this.cleanupDeps() // 清空依賴收集 這個過程也是尤為重要的,後續我會單獨寫一篇文章分析。
 }
 return value
 }

對於UserWatcher的初始化過程,我們基本上就分析完了,traverse函式本質就是一個遞迴函式,邏輯並不複雜,大家可以自行檢視。 初始化過程已經分析完,但現在我們好像並不知道watch到底是如何監聽data的資料變化的。其實對於UserWatcher的依賴收集,就發生在watcher.get方法中,通過this.getter(parsePath)函式,我們就訪問了vm例項上的屬性。因為這個時候已經initData,所以會觸發對應屬性的getter函式,這也是為什麼initData會放在initWatch和initComputed函式前面。所以當前的UserWatcher就會被存放進對應屬性Dep例項下的subs陣列中,如下:

Object.defineProperty(obj,{
 enumerable: true,configurable: true,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
 },}

前幾個篇章我們都提到renderWatcher,就是檢視的初始化渲染及更新所用。這個renderWathcer初始化的時機是在我們執行$mount方法的時候,這個時候又會對data上的資料進行了一遍依賴收集,每一個data的key的Dep例項都會將renderWathcer放到自己的subs陣列中。如圖:

詳解Vue中的watch和computed

,當我們對data上的資料進行修改時,就會觸發對應屬性的setter函式,進而觸發dep.notify(),遍歷subs中的每一個watcher,執行watcher.update()函式->watcher.run,renderWathcer的update方法我們就不深究了,不清楚的同學可以參考下我寫的Vue資料驅動。 對於我們分析的UserWatcher而言,相關程式碼如下:

class Watcher {
 constructor () {} //..
 run () {
 if (this.active) { // 用於標示watcher例項有沒有登出
  const value = this.get() // 執行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
  if (this.user) { // UserWatcher
   try {
   this.cb.call(this.vm,value,oldValue) // 執行回撥cb,並傳入新值和舊值作為引數
   } catch (e) {
   handleError(e,this.vm,`callback for watcher "${this.expression}"`)
   }
  } else {
   this.cb.call(this.vm,oldValue)
  }
  }
 }
 }
}

首先會判斷這個watcher是否已經登出,如果沒有則執行this.get方法,重新獲取一次新值,接著比較新值和舊值,如果相同則不繼續執行,若不同則執行在初始化時傳入的cb回撥函式,這裡其實就是handler函式。至此,UserWatcher的工作原理就分析完了。接下來我們來繼續分析ComputedWatcher,同樣的我們找到初始程式碼

Computed

initComputed

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component,computed: Object) {
 // $flow-disable-line
 const watchers = vm._computedWatchers = Object.create(null) // 用來存放computedWatcher的map
 // 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( // 執行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.
 if (!(key in vm)) { 
 // 會在vm的原型上去查詢computed對應的key值存不存在,如果不存在則執行defineComputed,存在的話則退出,
 // 這個地方其實是Vue精心設計的
 // 比如說一個元件在好幾個檔案中都引用了,如果不將computed
  defineComputed(vm,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)
  }
 }
 }
}

defineComputed

new Watcher的邏輯我們先放一邊,我們先關注一下defineComputed這個函式到底做了什麼

export function defineComputed (
 target: any,key: string,userDef: Object | Function
) {
 const shouldCache = !isServerRendering()
 if (typeof userDef === 'function') { // 分支1
 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
 }
 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,sharedPropertyDefinition)
}

這個函式本質也是呼叫Object.defineProperty來改寫computed的key值對應的getter函式和setter函式,當訪問到key的時候,就會觸發其對應的getter函式,對於大部分情況下,我們會走到分支1,對於不是服務端渲染而言,sharedPropertyDefinition.get會被createComputedGetter(key)賦值,set會被賦值為一個空函式。

createComputedGetter

function createComputedGetter (key) {
 return function computedGetter () {
 const watcher = this._computedWatchers && this._computedWatchers[key] // 就是上文中new Watcher()
 if (watcher) {
  if (watcher.dirty) {
  watcher.evaluate()
  }
  if (Dep.target) {
  watcher.depend()
  }
  return watcher.value
 }
 }
}

可以看到createComputedGetter(key)其實會返回一個computedGetter函式,也就是說在執行render函式時,訪問到這個vm[key]對應的computed的時候會觸發getter函式,而這個getter函式就是computedGetter。

<template>
	<div>{{ message }}</div>
</template>
export default {
	data () {
 	return {
  	a: 1,b: 2
  }
 },computed: {
 	message () { // 這裡的函式名message就是所謂的key
  	return this.a + this.b
  }
 }
}

以上程式碼為例子,來一步步解析computedGetter函式。 首先我們需要先獲取到key對應的watcher.

const watcher = this._computedWatchers && this._computedWatchers[key]

而這裡的watcher就是在initComputed函式中所生成的。

 if (!isSSR) { // 不是服務端渲染
  // create internal watcher for the computed property.
  watchers[key] = new Watcher( // 執行new Watcher
  vm,computedWatcherOptions { lazy: true }
  )
 }

我們來看看computedWatcher的初始化過程,我們還是接著來繼續回顧一下Watcher類相關程式碼

export default class Watcher {
 vm: Component;
 expression: string;
 cb: Function;
 id: number;
 deep: boolean;
 user: boolean;
 lazy: boolean;
 sync: boolean;
 dirty: boolean;
 active: boolean;
 deps: Array<Dep>;
 newDeps: Array<Dep>;
 depIds: SimpleSet;
 newDepIds: SimpleSet;
 before: ?Function;
 getter: Function;
 value: any;

 constructor (
 vm: Component,isRenderWatcher?: boolean
 ) {
 this.vm = vm
 if (isRenderWatcher) {
  vm._watcher = this
 }
 vm._watchers.push(this)
 // options
 if (options) {
  this.deep = !!options.deep
  this.user = !!options.user
  this.lazy = !!options.lazy // lazy = true
  this.sync = !!options.sync
  this.before = options.before
 } else {
  this.deep = this.user = this.lazy = this.sync = false
 }
 this.cb = cb
 this.id = ++uid // uid for batching
 this.active = true
 this.dirty = this.lazy // for lazy watchers this.dirty = true 這裡把this.dirty設定為true
 this.deps = []
 this.newDeps = []
 this.depIds = new Set()
 this.newDepIds = new Set()
 this.expression = process.env.NODE_ENV !== 'production'
  ? expOrFn.toString()
  : ''
 // parse expression for getter
 if (typeof expOrFn === 'function') { // 走到這一步
  this.getter = expOrFn
 } else {
  // ..
 }
 this.value = this.lazy // 一開始不執行this.get()函式 直接返回undefined
  ? undefined
  : this.get()
 }

緊接著回到computedGetter函式中,執行剩下的邏輯

if (watcher) {
 if (watcher.dirty) {
 watcher.evaluate()
 }
 if (Dep.target) {
 watcher.depend()
 }
 return watcher.value
}

首先判斷watcher是否存在,如果存在則執行以下操作

  • 判斷watcher.dirty是否為true,如果為true,則執行watcher.evaluate
  • 判斷當前Dep.target是否存在,存在則執行watcher.depend
  • 最後返回watcher.value

在computedWatcher初始化的時候,由於傳入的options.lazy為true,所以相應的watcher.diry也為true,當我們在執行render函式的時候,訪問到message,觸發了computedGetter,所以會執行watcher.evaluate。

evaluate () {
 this.value = this.get() // 這裡的get() 就是vm['message'] 返回就是this.a + this.b的和
 this.dirty = false // 將dirty置為false
}

同時這個時候由於訪問vm上的a屬性和b屬性,所以會觸發a和b的getter函式,這樣就會把當前這個computedWatcher加入到了a和b對應的Dpe例項下的subs陣列中了。如圖:

詳解Vue中的watch和computed

接著當前的Dep.target毫無疑問就是renderWatcher了,並且也是存在的,所以就執行了watcher.depend()

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

對於當前的message computedWatcher而言,this.deps其實就是a和b兩個屬性對應的Dep例項,接著遍歷整個deps,對每一個dep就進行depend()操作,也就是每一個Dep例項把當前的Dep.target(renderWatcher都加入到各自的subs中,如圖:

詳解Vue中的watch和computed

所以這個時候,一旦你修改了a和b的其中一個值,都會觸發setter函式->dep.notify()->watcher.update,程式碼如下:

update () {
 /* istanbul ignore else */
 if (this.lazy) {
 this.dirty = true
 } else if (this.sync) {
 this.run()
 } else {
 queueWatcher(this)
 }
}

總結

其實不管是watch還是computed本質上都是通過watcher來實現,只不過它們的依賴收集的時機會有所不同。就使用場景而言,computed多用於一個值依賴於其他響應式資料,而watch主要用於監聽響應式資料,在進行所需的邏輯操作!大家可以通過單步除錯的方法,一步步除錯,能更好地加深理解。

以上就是詳解Vue中的watch和computed的詳細內容,更多關於Vue watch和computed的資料請關注我們其它相關文章!