Vue之watch源碼解讀
回顧 watch 的用法
watch 是 Vue 中的一個監聽數據變化的一個方法,我們在閱讀源碼之前先來回顧一下 watch 的用法
監聽基本數據類型
<div>
{{ name }}
<button @click="changeName">改變name</button>
</div>
export default {
data() {
return {
name: 'maoxiaoxing',
}
},
watch: {
name(val, oldval) {
console.log(val, oldval)
}
},
methods: {
changeName() {
this.name = this.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'
}
}
}
watch 可以接收兩個參數,一個是變化之后的數據,一個是變化之前的數據,你可以基於這兩個值處理一些邏輯
監聽對象
<div>
{{ obj.name }}
<button @click="changeName">改變name</button>
</div>
export default {
data() {
return {
obj: {
name: 'maoxiaoxing',
}
}
},
watch: {
obj: {
handler(val, oldval) {
console.log(val, oldval)
},
deep: true,
immediate: true,
}
},
methods: {
changeName() {
this.obj.name = this.obj.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'
}
},
created() {
console.log('created')
}
}
在監聽對象變化的時候,加上 deep 這個屬性即可深度監聽對象數據;如果你想在頁面進來時就執行 watch 方法,加上 immediate 即可。值得注意的是,設置了 immediate 屬性的 watch 的執行順序是在 created 生命周期之前的
watch 接收參數為數組
我在看 Vue 源碼的時候,發現了一個比較有意思的地方,如果說 watch 監聽的屬性不去設置一個方法而是接收一個數組的話,可以向當前監聽的屬性傳遞多個方法
export default {
data() {
return {
name: 'jack',
}
},
watch: {
name: [
{ handler: function() {console.log(1)}, immediate: true },
function(val) {console.log(val, 2)}
]
},
methods: {
changeName() {
this.name = this.name === 'maoxiaoxing' ? 'yangxiaoA' : 'maoxiaoxing'
}
}
}
數組中可以接收不同形式的參數,可以是方法,也可以是一個對象,具體的書寫方式和普通的 watch 沒什么不同。可以接收數據為參數這一點在官方文檔沒有找到,至於為什么可以這樣寫,下面的源碼講解會提及。
初始化 watch
initState
// src\core\instance\state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // 如果有 props ,初始化 props
if (opts.methods) initMethods(vm, opts.methods) // 如果有 methods ,初始化 methods 里面的方法
if (opts.data) { // 如果有 data 的話,初始化,data;否則響應一個空對象
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed) // 如果有 computed ,初始化 computed
if (opts.watch && opts.watch !== nativeWatch) { // 如果有 watch ,初始化 watch
initWatch(vm, opts.watch)
}
}
首先在 initState 初始化 watch,如果有 watch 這個屬性的話,就將 watch 傳入 initWatch 方法中處理
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,我們可以看到 initWatch 會遍歷 watch,然后判斷每一個值是否是數組,如果是數組的就遍歷這個數組,創建多個回調函數,這塊也就解釋了上邊 watch 監聽的數據可以接收數組為參數原因;如果不是數組的話,就直接創建回調函數。從這里我們也能看到,我們學習源碼的好處,通過學習源碼,我們能學習到一些在官方文檔中沒有提到的寫法。
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 中會判斷 handler 是否是對象,如果是對象將 handler 掛載到 options 這個屬性,再將對象的 handler 屬性提取出來;如果 handler 是一個字符串的話,會從 Vue 實例找到這個方法賦值給 handler。從這里我們也能看出來,watch 還可以支持字符串的寫法。執行 Vue 實例上的 $watch 方法。
$watch
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// 獲取 Vue 實例 this
const vm: Component = this
if (isPlainObject(cb)) {
// 判斷如果 cb 是對象執行 createWatcher
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 標記為用戶 watcher
options.user = true
// 創建用戶 watcher 對象
const watcher = new Watcher(vm, expOrFn, cb, options)
// 判斷 immediate 如果為 true
if (options.immediate) {
// 立即執行一次 cb 回調,並且把當前值傳入
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 返回取消監聽的方法
return function unwatchFn () {
watcher.teardown()
}
}
$watch 函數是 Vue 的一個實例方法,也就是我們可以使用 Vue.$watch 去調用,這里不再過多贅述,官方文檔中講的很詳細。$watch 會創建一個 Watcher 對象,這塊也是涉及響應式原理,在 watch 中改變的數據可以進行數據的響應式變化。同時也會判斷是否有 immediate 這個屬性,如果有的話,就直接調用回調。
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
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 {
// expOrFn 是字符串的時候,例如 watch: { 'person.name': function... }
// parsePath('person.name') 返回一個函數獲取 person.name 的值
this.getter = parsePath(expOrFn)
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()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
/*獲得getter的值並且重新進行依賴收集*/
get () {
/*將自身watcher觀察者實例設置給Dep.target,用以依賴收集。*/
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
/*
執行了getter操作,看似執行了渲染操作,其實是執行了依賴收集。
在將Dep.target設置為自身觀察者實例以后,執行getter操作。
譬如說現在的的data中可能有a、b、c三個數據,getter渲染需要依賴a跟c,
那么在執行getter的時候就會觸發a跟c兩個數據的getter函數,
在getter函數中即可判斷Dep.target是否存在然后完成依賴收集,
將該觀察者對象放入閉包中的Dep的subs中去。
*/
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
/*如果存在deep,則觸發每個深層對象的依賴,追蹤其變化*/
if (this.deep) {
/*遞歸每一個對象或者數組,觸發它們的getter,使得對象或數組的每一個成員都被依賴收集,形成一個“深(deep)”依賴關系*/
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
... 其他方法
}
上面的 Watcher 我省略了一些其他方法,只保留了 get 函數,我們能在 get 函數中看到如果有 deep 屬性的話,就會遞歸處理對象中的每一個屬性,以達到深度監聽的效果。這里關於 watch 的使用和原理講解就完結了,我們通過閱讀源碼,不僅能夠了解 Vue 框架內部是怎樣實現的,同時也能看到一些官方文檔沒有提及的用法,對我們是很有幫助的。
