Watch Options 我們常用的選項:
immediate
:初始化馬上執行一次deep
:深層監聽once
:只執行一次,就停止監聽我們先寫接受三個參數,預設值是空的物件。
export function watch(source, cb, options) {
const { immediate, once, deep } = options || {}
...
...
}
當 immediate
選項為 true
時,watch
會在初始化階段立即執行一次 job
函式,此時的 callback 函式中 oldValue
為 undefined
。如果 immediate
為 false
(或未提供),則 watch
在初始化時僅只會執行 effect.run()
來收集依賴並取得初始的 oldValue
,但不會觸發 callback。
export function watch(source, cb, options) {
const { immediate, once, deep } = options || {}
...
...
if(immediate) {
// 第一次立即執行一次
job()
}else{
// 因為不是第一次執行,才會得到舊的資料,收集依賴
oldValue = effect.run() // 收集依賴後,得到 run 返回值,取得舊的數值
}
...
...
}
為了實現 once
功能,我們需要對使用者傳入的 callback 函式進行包裝。我們將原本的 callback 函式暫存起來,然後用一個新的匿名函式覆寫 cb
。
在這個新的函式中,我們先呼叫原始的 callback 函式,之後立即執行 stop()
函式,從而達到『執行一次後即停止』的效果。
export function watch(source, cb, options) {
const { immediate, once, deep } = options || {}
if(once) {
const _cb = cb
cb = (...args) => {
_cb(...args)
stop()
}
}
...
...
}
深層監聽(deep: true
)的原理是:在依賴收集階段,遍歷地訪問被監聽對象的所有巢狀屬性。這個過程會觸發每一個屬性的 getter
,從而將它們全部作為 watch
內部 effect
的依賴項進行收集。一旦任何深層屬性發生變化,watch
都能收到通知。
import { isObject } from '@vue/shared'
export function watch(source, cb, options) {
const { immediate, once, deep } = options || {}
...
...
if(deep){
const baseGetter = getter
getter = () => traverse(baseGetter())
}
...
}
function traverse(value) {
// 檢查類型
if(!isObject(value)) {
return
}
for(const key in value) {
traverse(value[key])
}
return value
}
這樣可以解決,但在使用上面有可能會遇到循環引用的問題,因此需要調整一下:
function traverse(value, seen = new Set()) {
if(!isObject(value)) {
return value
}
// 如果之前訪問過,就回傳原本的值,預防循環引用
if(seen.has(value)) {
return value
}
seen.add(value)
for(const key in value) {
traverse(value[key], seen)
}
return value
}
我們用 Set
結構來記錄在單次遍歷中所有已訪問過的物件。在遍歷到一個新物件前,先檢查它是不是存在 Set
中。
如果存在,說明遇到了循環引用,要立即停止目前的遞迴,從而避免堆疊溢出。
Deep 在3.5版本有一個新的功能,遇到巢狀物件監聽,可以指定監聽層級,像是下方範例:
<body>
<div id="app"></div>
<script type="module">
// import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { ref, watch } from '../dist/reactivity.esm.js'
const state = ref({
a: {
b: 1,
c: {
d: 1
}
}
})
watch(state, (newVal, oldVal) => {
console.log('newVal, oldVal', newVal, oldVal)
}, { deep: 2 })
setTimeout(() => {
state.value.a.c.d = 2
console.log('更新了')
}, 1000)
</script>
</body>
當 deep
的值是數字時,它代表了監聽的遞迴層級。例如 deep: 2
指的是監聽應深入到目標物件的第二層屬性。在上述範例中,修改 state.value.a.b
(第二層)應該觸發監聽,而修改 state.value.a.c.d
(第四層)則不應該觸發。
如果你切換到官方程式碼,控制台不會輸出任何結果。
if(deep){
const baseGetter = getter
const depth = deep === true ? Infinity : deep
getter = () => traverse(baseGetter(), depth)
}
function traverse(value, depth = Infinity, seen = new Set()) {
// 如果不是物件,或是監聽層級到了,就回傳原本的值
if(!isObject(value) || depth<=0) {
return value
}
// 如果之前訪問過,就回傳原本的值,預防循環引用
if(seen.has(value)) {
return value
}
depth--
seen.add(value)
for(const key in value) {
traverse(value[key],depth, seen)
}
return value
}
透過在遞迴函式 traverse
中傳遞並遞減 depth
計數,我們就能精確控制依賴收集的深度。
我們把剛剛的程式碼改成 reactive 之後,發現控制台報錯
確認一下官方的解決方案:
<body>
<div id="app"></div>
<script type="module">
import { reactive, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
// import { reactive, watch } from '../dist/reactivity.esm.js'
const state = reactive({
a: {
b: 1,
c: {
d: 1
}
}
})
watch(state, (newVal, oldVal) => {
console.log('newVal, oldVal', newVal, oldVal)
},
// { deep: 2 }
)
setTimeout(() => {
state.a.c.d = 2
console.log('更新了')
}, 1000)
</script>
</body>
查看控制台你會發現,當 watch
的監聽來源是 reactive
物件時,deep
選項會預設為 true
。
因此,我們需要調整 getter
的初始化邏輯來應對此情況:
reactive
物件時,getter
應直接返回該物件deep
選項,則應將 deep
的值預設為 true
。所以我們接下來要做:
如果 reactive 傳入,預設 deep:true
,如果有傳入層級,以傳入層級為主。
if(isRef(source)) { // source 有可能是 ref 物件,進行函式的包裝
getter = () => source.value
}else if(isReactive(source)){
// 如果 source 是 reactive,直接賦值給 getter
getter = () => source
if(!deep) deep = true
// 如果 source 是函式,直接賦值給 getter
}else if(isFunction(source)){
getter = source
}
這樣就不會報錯,而且 reactive
也預設監聽。
我們目前已經完成 watch
的 options
實作,除了擴充了 immediate
、once
、deep
等常用方法,我們還透過遞迴遍歷與 Set
解決了深度監聽中的循環引用問題,並解決了對 ref
、reactive
及 getter
函式等多種來源的處理。
同步更新《嘿,日安!》技術部落格
if(!deep) deep = true
若 deep: 0
的話會跟 vue 表現不一樣😂
可以調整成if (deep === undefined) deep = true