Vue3 - 每天來一點雷 Part 2

受到疫情影響,絕大部分的時間不是在公司就是在家裡,每個禮拜出門採買一次。其實也不是說我不喜歡用線上購物,只是目前其實物流業也是相當緊繃。出門自己小心避開人潮,萬事小心謹慎點就好了。

至於避開人潮這件事情,我都平常日早上 8 點多去大潤發,大概可以避開八九成的人流。


I'm watching you

在 Vue3.x 之後呢,除了原有的 watch 之外,另外提供了一個叫做 watchEffect 的方法。前者跟 Vue2.x 的區別在於,在 3.x 以後深度監聽已經是預設,所以你不需要額外設定(但是)。而 watchEffect 在此則是全新的方法。

兩者的共通點:

  • 響應式數值發生變化時觸發。
  • 開發者可以執行指定函式(好像是廢話)。
  • 提供一個方法讓開發者可終止函式(你可以想成 clearTimeout 之類的事情)。
  • 終止函式隱含 onInvalidate 方法可以使用。

而兩者差異則有:

  • watch 本身屬於懶加載,所以第一次運行的時候不會動作。
  • watch 提供新、舊數值內容(第三個參數則用來傳遞終止方法)。
  • watchEffect 會立即執行,也就是第一次運行的時候就會觸發。
  • watchEffect 自動監聽所有的響應式變數,你不用特別指定。

watch

由於在 3.x 的版本開始,預設就是深度監聽,所以,在使用上請特別小心。深度監聽到底做了什麼事情這邊就不贅述,請不要做那種我跳進來啦,我又跳出去啦的這種事情。

例如(以下範例地獄無窮迴圈,請不要隨意嘗試):

const counter = ref(0)

watch(counter, (newVal, oldVal) => {

    console.log('我跳進來啦', newVal)
    
    counter.value += 1
    
    console.log('我又跳出去啦', counter.value)
})

又或者是,我們在搭配 Vue-Router 的時候,多半會去監聽網站的路徑是否有什麼變化,這個時候如果想不開,就會出現路由無限重複導向的狀況。範例就不列出來了,有經驗的人應該知道我再說什麼事情。

根據 Vue3.x 原始碼中所提到,他在執行 watch 的 queue 是使用 { flash: 'post', deep: true } 的預設參數去做設定,所以一開始才會說他是預設使用深度監聽。但是,

這個深度監聽是有些條件限制的。

首先,你所監聽的對象必須要擁有 getter/setter 的物件,也就是說,以下這樣的方式是可行的,

import { ref, watch } from 'vue'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const myVariable = ref(0)
        
        watch(myVariable, (newVal) => {
            console.log(newVal)
        })
        
        setTimeout(() => {
            myVariable.value += 1
        }, 1000)
        
        return {
            myVariable
        }
    }
}

當你把對象稍微調整一下,改由 reactive() 來做的時候,

import { reactive, watch } from 'vue'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const myVariable = reactive({
            counter: 1
        })
        
        watch(myVariable, (newVal) => {
            console.log(newVal)
        })
        
        setTimeout(() => {
            myVariable.counter += 1
        }, 1000)
        
        return {
            myVariable
        }
    }
}

然後你就會發現 watch 就不理你了。說好的預設是深度監聽呢?,其實 官方文件 有交代這些事情,可以的話回去喵一眼再回來看看這裡為何不會動。

所以基本上你還是必須做一個深度拷貝,或諸如此類的事情,這樣就會動作了,

import { reactive, watch } from 'vue'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const myVariable = reactive({
            counter: 1
        })
        
        watch(() => ({...myVariable}), (newVal) => {
            console.log(newVal)
        }, {
            deep: true
        })
        
        setTimeout(() => {
            myVariable.counter += 1
        }, 1000)
        
        return {
            myVariable
        }
    }
}


watchEffect

基本上跟 watch 很相似,你可以把他想成設定了 immediatewatch 的效果這樣。不過與 watch 稍微不同的地方在於,剛剛的例子,是會有反應的,

import { reactive, watch } from 'vue'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const myVariable = reactive({
            counter: 1
        })
        
        watchEffect(() => {
            console.log(newVal)
        })
        
        setTimeout(() => {
            myVariable.counter += 1
        }, 1000)
        
        return {
            myVariable
        }
    }
}

他也不需要加什麼 { deep: true } 這件事,感覺起來是不是比 watch 還要好用?具體上來說,這兩個方法應用的面向不太一樣,當然你想要全部都用某一種也是沒什麼關係。這裡沒有一定很好的作法,只是你需要考量監聽的對象,與是否需要持續監聽動作而已。


onInvalidate

無論是 watch 或是 watchEffect 都有這第三個參數可以使用,他的用意在於當監聽被取消時,這個方法就會被呼叫。

import { reactive, watch, watchEffect } from 'vue'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const myVariable = ref(0)
        
        const stopWatchEffect = watchEffect((onInvalidate) => {
            console.log(newVal)
            
            onInvalidate(() => { ... })
        })

        const stopWatch = watch(myVariable, (newVal, oldVal, onInvalidate) => {
            console.log(newVal)
            
            onInvalidate(() => { ... })
        })

        setTimeout(() => {
            myVariable.value += 1
        }, 1000)
        
        return {
            myVariable
        }
    }
}

但有一個比較需要小心的地方,這一點需要貼一下 Vue3.x 原始碼給大家看一下,

let onInvalidate = (fn) => {
  cleanup = runner.options.onStop = () => {
      callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */);
  };
};

// 中略...

if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
    // cleanup before running cb again
    if (cleanup) {
        cleanup();
    }
    callWithAsyncErrorHandling(cb, instance, 3 /* WATCH_CALLBACK */, [
        newValue,
        // pass undefined as the old value when it's changed for the first time
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
        onInvalidate
    ]);
    oldValue = newValue;
}

如果你監聽的物件不是在你的元件內,例如說,

import { reactive, watch, watchEffect } from 'vue'

import useWindowResize from './useWindowResize.js'

export default {
    name: 'ExampleComponent',
    setup () {
    
        const myVariable = useWindowResize()
        
        const stopWatchEffect = watchEffect((onInvalidate) => {
            console.log(myVariable)
            
            onInvalidate(() => { ... })
        })

        const stopWatch = watch(() => ({...myVariable}), (newVal, oldVal, onInvalidate) => {
            console.log(newVal)
            
            onInvalidate(() => { ... })
        }, {
            deep: true
        })

        setTimeout(() => {
            myVariable.value += 1
        }, 1000)
        
        return {
            myVariable
        }
    }
}

以上有幾個雷點,

  1. watchEffect 基本上看不到變化,這個時候 watch 才會聽得到改變。
  2. onInvalidate() 會因為 myVariable 的變化而一直被呼叫。
  3. stopWatch 被呼叫後,此時 watch 才會停止監聽。

關於第 2 點,請務必留意。至於為什麼?我剛剛原始碼已經有貼上了,看不懂的話可以問一下官方為何要這樣設計。


小結

其實這次 Vue3.x 還有兩個 onTrack, onTrigger 可用,主要目的是用來除錯用的,由於我超不會除錯所以這邊就不介紹這麼多了。

請記得 onInvalidate() 小心使用。

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei