詳解Vue中的watch和computed
前言
對于使用Vue的前端而言,watch、computed和methods三個屬性相信是不陌生的,是日常開發(fā)中經(jīng)常使用的屬性。但是對于它們的區(qū)別及使用場景,又是否清楚,本文我將跟大家一起通過源碼來分析這三者的背后實現(xiàn)原理,更進一步地理解它們所代表的含義。 在繼續(xù)閱讀本文之前,希望你已經(jīng)具備了一定的Vue使用經(jīng)驗,如果想學習Vue相關知識,請移步至官網(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 重點
} 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初始化之前,這是為什么呢?大家可以停在這里想一下這個問題。想不通也沒關系,繼續(xù)接下來的源碼分析,這個問題也會迎刃而解。
initWatch
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) { // 如果handler是一個數(shù)組
for (let i = 0; i < handler.length; i++) { // 遍歷watch的每一項,執(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是否是純對象,對options和handler重新賦值
options = handler
handler = handler.handler
}
if (typeof handler === 'string') { // handler用的是methods上面的方法,具體用法請查看官網(wǎng)文檔
handler = vm[handler]
}
// expOrnFn: watch的key值, handler: 回調函數(shù) 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是否是對象,如果是則繼續(xù)調用createWatcher
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true // user Watcher的標示 options = { user: true, ...options }
const watcher = new Watcher(vm, expOrFn, cb, options) // new Watcher 生成一個user Watcher
if (options.immediate) { // 如果傳入了immediate 則直接執(zhí)行回調cb
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
}
上面幾個函數(shù)調用的邏輯都比較簡單,所以就在代碼上寫了注釋。我們重點關注一下這個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,
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的時候傳入了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' // 一個函數(shù)表達式
? 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而言,不是函數(shù)所以會執(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]] // 每次把當前的key值對應的值重新賦值obj
}
return obj
}
}
首先會判斷傳入的path是否符合預期,如果不符合則直接return,接著講path根據(jù)'.'字符串進行拆分,因為我們傳入的watch可能有如下幾種形式:
watch: {
a: () {}
'formData.a': () {}
}
所以需要對path進行拆分,接下來遍歷拆分后的數(shù)組,這里返回的函數(shù)的參數(shù)obj其實就是vm實例,通過vm[segments[i]],就可以最終找到這個watch所對應的屬性,最后將obj返回。
constructor () { // 初始化的最后一段邏輯
this.value = this.lazy // 因為this.lazy為false,所以會執(zhí)行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放回的函數(shù),并將vm實例當做第一個參數(shù)傳入
} catch (e) {
if (this.user) {
handleError(e, vm, `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,則執(zhí)行深遞歸
traverse(value)
}
popTarget() // 將當前watch出棧
this.cleanupDeps() // 清空依賴收集 這個過程也是尤為重要的,后續(xù)我會單獨寫一篇文章分析。
}
return value
}
對于UserWatcher的初始化過程,我們基本上就分析完了,traverse函數(shù)本質就是一個遞歸函數(shù),邏輯并不復雜,大家可以自行查看。 初始化過程已經(jīng)分析完,但現(xiàn)在我們好像并不知道watch到底是如何監(jiān)聽data的數(shù)據(jù)變化的。其實對于UserWatcher的依賴收集,就發(fā)生在watcher.get方法中,通過this.getter(parsePath)函數(shù),我們就訪問了vm實例上的屬性。因為這個時候已經(jīng)initData,所以會觸發(fā)對應屬性的getter函數(shù),這也是為什么initData會放在initWatch和initComputed函數(shù)前面。所以當前的UserWatcher就會被存放進對應屬性Dep實例下的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
},
}
前幾個篇章我們都提到renderWatcher,就是視圖的初始化渲染及更新所用。這個renderWathcer初始化的時機是在我們執(zhí)行$mount方法的時候,這個時候又會對data上的數(shù)據(jù)進行了一遍依賴收集,每一個data的key的Dep實例都會將renderWathcer放到自己的subs數(shù)組中。如圖:

, 當我們對data上的數(shù)據(jù)進行修改時,就會觸發(fā)對應屬性的setter函數(shù),進而觸發(fā)dep.notify(),遍歷subs中的每一個watcher,執(zhí)行watcher.update()函數(shù)->watcher.run,renderWathcer的update方法我們就不深究了,不清楚的同學可以參考下我寫的Vue數(shù)據(jù)驅動。 對于我們分析的UserWatcher而言,相關代碼如下:
class Watcher {
constructor () {} //..
run () {
if (this.active) { // 用于標示watcher實例有沒有注銷
const value = this.get() // 執(zhí)行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) // 執(zhí)行回調cb,并傳入新值和舊值作為參數(shù)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
}
首先會判斷這個watcher是否已經(jīng)注銷,如果沒有則執(zhí)行this.get方法,重新獲取一次新值,接著比較新值和舊值,如果相同則不繼續(xù)執(zhí)行,若不同則執(zhí)行在初始化時傳入的cb回調函數(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) { // 不是服務端渲染
// 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)) {
// 會在vm的原型上去查找computed對應的key值存不存在,如果不存在則執(zhí)行defineComputed,存在的話則退出,
// 這個地方其實是Vue精心設計的
// 比如說一個組件在好幾個文件中都引用了,如果不將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的邏輯我們先放一邊,我們先關注一下defineComputed這個函數(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)
}
這個函數(shù)本質也是調用Object.defineProperty來改寫computed的key值對應的getter函數(shù)和setter函數(shù),當訪問到key的時候,就會觸發(fā)其對應的getter函數(shù),對于大部分情況下,我們會走到分支1,對于不是服務端渲染而言,sharedPropertyDefinition.get會被createComputedGetter(key)賦值,set會被賦值為一個空函數(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)其實會返回一個computedGetter函數(shù),也就是說在執(zhí)行render函數(shù)時,訪問到這個vm[key]對應的computed的時候會觸發(fā)getter函數(shù),而這個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對應的watcher.
const watcher = this._computedWatchers && this._computedWatchers[key]
而這里的watcher就是在initComputed函數(shù)中所生成的。
if (!isSSR) { // 不是服務端渲染
// create internal watcher for the computed property.
watchers[key] = new Watcher( // 執(zhí)行new Watcher
vm,
getter || noop,
noop,
computedWatcherOptions { lazy: true }
)
}
我們來看看computedWatcher的初始化過程,我們還是接著來繼續(xù)回顧一下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,
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設置為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
- 判斷當前Dep.target是否存在,存在則執(zhí)行watcher.depend
- 最后返回watcher.value
在computedWatcher初始化的時候,由于傳入的options.lazy為true,所以相應的watcher.diry也為true,當我們在執(zhí)行render函數(shù)的時候,訪問到message,觸發(fā)了computedGetter,所以會執(zhí)行watcher.evaluate。
evaluate () {
this.value = this.get() // 這里的get() 就是vm['message'] 返回就是this.a + this.b的和
this.dirty = false // 將dirty置為false
}
同時這個時候由于訪問vm上的a屬性和b屬性,所以會觸發(fā)a和b的getter函數(shù),這樣就會把當前這個computedWatcher加入到了a和b對應的Dpe實例下的subs數(shù)組中了。如圖:

接著當前的Dep.target毫無疑問就是renderWatcher了,并且也是存在的,所以就執(zhí)行了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中,如圖:

所以這個時候,一旦你修改了a和b的其中一個值,都會觸發(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)
}
}
總結
其實不管是watch還是computed本質上都是通過watcher來實現(xiàn),只不過它們的依賴收集的時機會有所不同。就使用場景而言,computed多用于一個值依賴于其他響應式數(shù)據(jù),而watch主要用于監(jiān)聽響應式數(shù)據(jù),在進行所需的邏輯操作!大家可以通過單步調試的方法,一步步調試,能更好地加深理解。
以上就是詳解Vue中的watch和computed的詳細內容,更多關于Vue watch和computed的資料請關注腳本之家其它相關文章!
相關文章
element-ui?table使用type='selection'復選框全禁用(全選禁用)詳解
element-ui中的table的多選很好用,但是如果其中某一項禁止選擇,該怎樣操作呢,下面這篇文章主要給大家介紹了關于element-ui?table使用type='selection'復選框全禁用(全選禁用)的相關資料,需要的朋友可以參考下2023-01-01
Vue實現(xiàn)兩個列表之間的數(shù)據(jù)聯(lián)動的代碼示例
在Vue.js應用開發(fā)中,列表數(shù)據(jù)的聯(lián)動是一個常見的需求,這種聯(lián)動可以用于多種場景,例如過濾篩選、關聯(lián)選擇等,本文將詳細介紹如何在Vue項目中實現(xiàn)兩個列表之間的數(shù)據(jù)聯(lián)動,并通過多個具體的代碼示例來幫助讀者理解其實現(xiàn)過程,需要的朋友可以參考下2024-10-10

