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