詳解Vue中的watch和computed
前言
對(duì)于使用Vue的前端而言,watch、computed和methods三個(gè)屬性相信是不陌生的,是日常開發(fā)中經(jīng)常使用的屬性。但是對(duì)于它們的區(qū)別及使用場景,又是否清楚,本文我將跟大家一起通過源碼來分析這三者的背后實(shí)現(xiàn)原理,更進(jìn)一步地理解它們所代表的含義。 在繼續(xù)閱讀本文之前,希望你已經(jīng)具備了一定的Vue使用經(jīng)驗(yàn),如果想學(xué)習(xí)Vue相關(guān)知識(shí),請(qǐng)移步至官網(wǎng)。
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 重點(diǎn) } 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的作用,不過在接下去之前,這里有一點(diǎn)是data的初始化是在computed和watch初始化之前,這是為什么呢?大家可以停在這里想一下這個(gè)問題。想不通也沒關(guān)系,繼續(xù)接下來的源碼分析,這個(gè)問題也會(huì)迎刃而解。
initWatch
function initWatch (vm: Component, watch: Object) { for (const key in watch) { const handler = watch[key] if (Array.isArray(handler)) { // 如果handler是一個(gè)數(shù)組 for (let i = 0; i < handler.length; i++) { // 遍歷watch的每一項(xiàng),執(zhí)行createWatcher createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } } }
createWatcher
function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { if (isPlainObject(handler)) { // 判斷handler是否是純對(duì)象,對(duì)options和handler重新賦值 options = handler handler = handler.handler } if (typeof handler === 'string') { // handler用的是methods上面的方法,具體用法請(qǐng)查看官網(wǎng)文檔 handler = vm[handler] } // expOrnFn: watch的key值, handler: 回調(diào)函數(shù) options: 可選配置 return vm.$watch(expOrFn, handler, options) // 調(diào)用原型上的$watch }
Vue.prototype.$watch
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this if (isPlainObject(cb)) { // 判斷cb是否是對(duì)象,如果是則繼續(xù)調(diào)用createWatcher return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true // user Watcher的標(biāo)示 options = { user: true, ...options } const watcher = new Watcher(vm, expOrFn, cb, options) // new Watcher 生成一個(gè)user Watcher if (options.immediate) { // 如果傳入了immediate 則直接執(zhí)行回調(diào)cb try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } return function unwatchFn () { watcher.teardown() } } }
上面幾個(gè)函數(shù)調(diào)用的邏輯都比較簡單,所以就在代碼上寫了注釋。我們重點(diǎn)關(guān)注一下這個(gè)userWatcher生成的時(shí)候做了什么。
Watcher
又來到了我們比較常見的Watcher類的階段了,這次我們重點(diǎn)關(guān)注生成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, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { // 在 new UserWatcher的時(shí)候傳入了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' // 一個(gè)函數(shù)表達(dá)式 ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) // 進(jìn)入這個(gè)邏輯,調(diào)用parsePath方法,對(duì)getter進(jìn)行賦值 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() } }
首先會(huì)對(duì)這個(gè)watcher的屬性進(jìn)行一系列的初始化配置,接著判斷expOrFn這個(gè)值,對(duì)于我們watch的key而言,不是函數(shù)所以會(huì)執(zhí)行parsePath函數(shù),該函數(shù)定義如下:
/** * 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++) { // 遍歷數(shù)組 if (!obj) return obj = obj[segments[i]] // 每次把當(dāng)前的key值對(duì)應(yīng)的值重新賦值obj } return obj } }
首先會(huì)判斷傳入的path是否符合預(yù)期,如果不符合則直接return,接著講path根據(jù)'.'字符串進(jìn)行拆分,因?yàn)槲覀儌魅氲膚atch可能有如下幾種形式:
watch: { a: () {} 'formData.a': () {} }
所以需要對(duì)path進(jìn)行拆分,接下來遍歷拆分后的數(shù)組,這里返回的函數(shù)的參數(shù)obj其實(shí)就是vm實(shí)例,通過vm[segments[i]],就可以最終找到這個(gè)watch所對(duì)應(yīng)的屬性,最后將obj返回。
constructor () { // 初始化的最后一段邏輯 this.value = this.lazy // 因?yàn)閠his.lazy為false,所以會(huì)執(zhí)行this.get方法 ? undefined : this.get() } get () { pushTarget(this) // 將當(dāng)前的watcher實(shí)例賦值給 Dep.target let value const vm = this.vm try { value = this.getter.call(vm, vm) // 這里的getter就是上文所講parsePath放回的函數(shù),并將vm實(shí)例當(dāng)做第一個(gè)參數(shù)傳入 } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) // 如果報(bào)錯(cuò)了會(huì)這這一塊邏輯 } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { // 如果deep為true,則執(zhí)行深遞歸 traverse(value) } popTarget() // 將當(dāng)前watch出棧 this.cleanupDeps() // 清空依賴收集 這個(gè)過程也是尤為重要的,后續(xù)我會(huì)單獨(dú)寫一篇文章分析。 } return value }
對(duì)于UserWatcher的初始化過程,我們基本上就分析完了,traverse函數(shù)本質(zhì)就是一個(gè)遞歸函數(shù),邏輯并不復(fù)雜,大家可以自行查看。 初始化過程已經(jīng)分析完,但現(xiàn)在我們好像并不知道watch到底是如何監(jiān)聽data的數(shù)據(jù)變化的。其實(shí)對(duì)于UserWatcher的依賴收集,就發(fā)生在watcher.get方法中,通過this.getter(parsePath)函數(shù),我們就訪問了vm實(shí)例上的屬性。因?yàn)檫@個(gè)時(shí)候已經(jīng)initData,所以會(huì)觸發(fā)對(duì)應(yīng)屬性的getter函數(shù),這也是為什么initData會(huì)放在initWatch和initComputed函數(shù)前面。所以當(dāng)前的UserWatcher就會(huì)被存放進(jìn)對(duì)應(yīng)屬性Dep實(shí)例下的subs數(shù)組中,如下:
Object.defineProperty(obj, key, { 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 }, }
前幾個(gè)篇章我們都提到renderWatcher,就是視圖的初始化渲染及更新所用。這個(gè)renderWathcer初始化的時(shí)機(jī)是在我們執(zhí)行$mount方法的時(shí)候,這個(gè)時(shí)候又會(huì)對(duì)data上的數(shù)據(jù)進(jìn)行了一遍依賴收集,每一個(gè)data的key的Dep實(shí)例都會(huì)將renderWathcer放到自己的subs數(shù)組中。如圖:
, 當(dāng)我們對(duì)data上的數(shù)據(jù)進(jìn)行修改時(shí),就會(huì)觸發(fā)對(duì)應(yīng)屬性的setter函數(shù),進(jìn)而觸發(fā)dep.notify(),遍歷subs中的每一個(gè)watcher,執(zhí)行watcher.update()函數(shù)->watcher.run,renderWathcer的update方法我們就不深究了,不清楚的同學(xué)可以參考下我寫的Vue數(shù)據(jù)驅(qū)動(dòng)。 對(duì)于我們分析的UserWatcher而言,相關(guān)代碼如下:
class Watcher { constructor () {} //.. run () { if (this.active) { // 用于標(biāo)示watcher實(shí)例有沒有注銷 const value = this.get() // 執(zhí)行g(shù)et方法 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) // 執(zhí)行回調(diào)cb,并傳入新值和舊值作為參數(shù) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } } }
首先會(huì)判斷這個(gè)watcher是否已經(jīng)注銷,如果沒有則執(zhí)行this.get方法,重新獲取一次新值,接著比較新值和舊值,如果相同則不繼續(xù)執(zhí)行,若不同則執(zhí)行在初始化時(shí)傳入的cb回調(diào)函數(shù),這里其實(shí)就是handler函數(shù)。至此,UserWatcher的工作原理就分析完了。接下來我們來繼續(xù)分析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) { // 不是服務(wù)端渲染 // create internal watcher for the computed property. watchers[key] = new Watcher( // 執(zhí)行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)) { // 會(huì)在vm的原型上去查找computed對(duì)應(yīng)的key值存不存在,如果不存在則執(zhí)行defineComputed,存在的話則退出, // 這個(gè)地方其實(shí)是Vue精心設(shè)計(jì)的 // 比如說一個(gè)組件在好幾個(gè)文件中都引用了,如果不將computed defineComputed(vm, key, 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的邏輯我們先放一邊,我們先關(guān)注一下defineComputed這個(gè)函數(shù)到底做了什么
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, key, sharedPropertyDefinition) }
這個(gè)函數(shù)本質(zhì)也是調(diào)用Object.defineProperty來改寫computed的key值對(duì)應(yīng)的getter函數(shù)和setter函數(shù),當(dāng)訪問到key的時(shí)候,就會(huì)觸發(fā)其對(duì)應(yīng)的getter函數(shù),對(duì)于大部分情況下,我們會(huì)走到分支1,對(duì)于不是服務(wù)端渲染而言,sharedPropertyDefinition.get會(huì)被createComputedGetter(key)賦值,set會(huì)被賦值為一個(gè)空函數(shù)。
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)其實(shí)會(huì)返回一個(gè)computedGetter函數(shù),也就是說在執(zhí)行render函數(shù)時(shí),訪問到這個(gè)vm[key]對(duì)應(yīng)的computed的時(shí)候會(huì)觸發(fā)getter函數(shù),而這個(gè)getter函數(shù)就是computedGetter。
<template> <div>{{ message }}</div> </template> export default { data () { return { a: 1, b: 2 } }, computed: { message () { // 這里的函數(shù)名message就是所謂的key return this.a + this.b } } }
以上代碼為例子,來一步步解析computedGetter函數(shù)。 首先我們需要先獲取到key對(duì)應(yīng)的watcher.
const watcher = this._computedWatchers && this._computedWatchers[key]
而這里的watcher就是在initComputed函數(shù)中所生成的。
if (!isSSR) { // 不是服務(wù)端渲染 // create internal watcher for the computed property. watchers[key] = new Watcher( // 執(zhí)行new Watcher vm, getter || noop, noop, computedWatcherOptions { lazy: true } ) }
我們來看看computedWatcher的初始化過程,我們還是接著來繼續(xù)回顧一下Watcher類相關(guān)代碼
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, expOrFn: string | Function, cb: Function, options?: ?Object, 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設(shè)置為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 // 一開始不執(zhí)行this.get()函數(shù) 直接返回undefined ? undefined : this.get() }
緊接著回到computedGetter函數(shù)中,執(zhí)行剩下的邏輯
if (watcher) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value }
首先判斷watcher是否存在,如果存在則執(zhí)行以下操作
- 判斷watcher.dirty是否為true,如果為true,則執(zhí)行watcher.evaluate
- 判斷當(dāng)前Dep.target是否存在,存在則執(zhí)行watcher.depend
- 最后返回watcher.value
在computedWatcher初始化的時(shí)候,由于傳入的options.lazy為true,所以相應(yīng)的watcher.diry也為true,當(dāng)我們?cè)趫?zhí)行render函數(shù)的時(shí)候,訪問到message,觸發(fā)了computedGetter,所以會(huì)執(zhí)行watcher.evaluate。
evaluate () { this.value = this.get() // 這里的get() 就是vm['message'] 返回就是this.a + this.b的和 this.dirty = false // 將dirty置為false }
同時(shí)這個(gè)時(shí)候由于訪問vm上的a屬性和b屬性,所以會(huì)觸發(fā)a和b的getter函數(shù),這樣就會(huì)把當(dāng)前這個(gè)computedWatcher加入到了a和b對(duì)應(yīng)的Dpe實(shí)例下的subs數(shù)組中了。如圖:
接著當(dāng)前的Dep.target毫無疑問就是renderWatcher了,并且也是存在的,所以就執(zhí)行了watcher.depend()
depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } }
對(duì)于當(dāng)前的message computedWatcher而言,this.deps其實(shí)就是a和b兩個(gè)屬性對(duì)應(yīng)的Dep實(shí)例,接著遍歷整個(gè)deps,對(duì)每一個(gè)dep就進(jìn)行depend()操作,也就是每一個(gè)Dep實(shí)例把當(dāng)前的Dep.target(renderWatcher都加入到各自的subs中,如圖:
所以這個(gè)時(shí)候,一旦你修改了a和b的其中一個(gè)值,都會(huì)觸發(fā)setter函數(shù)->dep.notify()->watcher.update,代碼如下:
update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
總結(jié)
其實(shí)不管是watch還是computed本質(zhì)上都是通過watcher來實(shí)現(xiàn),只不過它們的依賴收集的時(shí)機(jī)會(huì)有所不同。就使用場景而言,computed多用于一個(gè)值依賴于其他響應(yīng)式數(shù)據(jù),而watch主要用于監(jiān)聽響應(yīng)式數(shù)據(jù),在進(jìn)行所需的邏輯操作!大家可以通過單步調(diào)試的方法,一步步調(diào)試,能更好地加深理解。
以上就是詳解Vue中的watch和computed的詳細(xì)內(nèi)容,更多關(guān)于Vue watch和computed的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue打印瀏覽器頁面功能的兩種實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于vue打印瀏覽器頁面功能的兩種實(shí)現(xiàn)方法,這個(gè)功能其實(shí)也是自己學(xué)習(xí)到的,做完也有一段時(shí)間了,一直想記錄總結(jié)一下,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04vue新建項(xiàng)目并配置標(biāo)準(zhǔn)路由過程解析
這篇文章主要介紹了vue新建項(xiàng)目并配置標(biāo)準(zhǔn)路由過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12利用vue開發(fā)一個(gè)所謂的數(shù)獨(dú)方法實(shí)例
數(shù)獨(dú)是源自18世紀(jì)瑞士的一種數(shù)學(xué)游戲,是一種運(yùn)用紙、筆進(jìn)行演算的邏輯游戲。下面這篇文章主要給大家介紹了關(guān)于利用vue開發(fā)一個(gè)所謂的數(shù)獨(dú)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-12-12vue實(shí)現(xiàn)手機(jī)驗(yàn)證碼登錄
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)手機(jī)驗(yàn)證碼登錄,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11關(guān)于vue跳轉(zhuǎn)后頁面置頂?shù)膯栴}
這篇文章主要介紹了關(guān)于vue跳轉(zhuǎn)后頁面置頂?shù)膯栴},具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05element-ui?table使用type='selection'復(fù)選框全禁用(全選禁用)詳解
element-ui中的table的多選很好用,但是如果其中某一項(xiàng)禁止選擇,該怎樣操作呢,下面這篇文章主要給大家介紹了關(guān)于element-ui?table使用type='selection'復(fù)選框全禁用(全選禁用)的相關(guān)資料,需要的朋友可以參考下2023-01-01Vue實(shí)現(xiàn)兩個(gè)列表之間的數(shù)據(jù)聯(lián)動(dòng)的代碼示例
在Vue.js應(yīng)用開發(fā)中,列表數(shù)據(jù)的聯(lián)動(dòng)是一個(gè)常見的需求,這種聯(lián)動(dòng)可以用于多種場景,例如過濾篩選、關(guān)聯(lián)選擇等,本文將詳細(xì)介紹如何在Vue項(xiàng)目中實(shí)現(xiàn)兩個(gè)列表之間的數(shù)據(jù)聯(lián)動(dòng),并通過多個(gè)具體的代碼示例來幫助讀者理解其實(shí)現(xiàn)過程,需要的朋友可以參考下2024-10-10