iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
Vue.js

從零到一打造 Vue3 響應式系統系列 第 25

Day 25 - Watch :清理 SideEffect

  • 分享至 

  • xImage
  •  

banner
watch 的一個核心用途是在響應式資料發生改變時,執行 Side Effect。

然而,當 Side Effect 是非同步或需要手動清理時,就會出現一個常見的問題:如果監聽的資料在短時間內多次變更,前一次的 Side Effect可能沒清理乾淨,就會與下一次的 Side Effect 產生衝突或造成資源洩漏。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <title>Document</title>
  <style>
    #app,#div{
      width: 100px;
      height: 100px;
      background-color: red;
      margin-bottom: 10px;
    }
    #div{
      background-color: blue;
    }
  </style>
</head>

<body>
  <div id="app"></div>
  <div id="div"></div>
  <button id="button">按鈕</button>
  <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 flag = ref(true)

      watch(flag, (newVal, oldVal) => {
        const dom = newVal ? app : div
      
        function handler () {
          console.log(newVal ? '點擊app' : '點擊div')
        }

        dom.addEventListener('click', handler)
    }, 
    { immediate: true }
  )

  button.onclick = () => {
    flag.value = !flag.value
  }

  </script>
</body>

</html>

上述範例是一個常見的資源洩漏問題:現在可以看到有兩個色塊,點了app 會被觸發,點擊 div 沒有反應,點擊按鈕,當 flagtrue 變為 false 時,此時div 點擊後會被觸發,但你點擊 app 控制台仍然有輸出,這是因為app 元素上註冊的 click 事件監聽器並沒有被移除。

因此,即使邏輯上它不應該再響應點擊,但 click 監聽器依然殘留在記憶體中並繼續觸發。

官方的解決方案是,有一個onCleanup函式:

<body>
  <div id="app"></div>
  <div id="div"></div>
  <button id="button">按鈕</button>
  <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 flag = ref(true)

      watch(flag, (newVal, oldVal, onCleanup) => {
        const dom = newVal ? app : div
      
        function handler () {
          console.log(newVal ? '點擊app' : '點擊div')
        }

        dom.addEventListener('click', handler)

      onCleanup(() => {
        dom.removeEventListener('click', handler)
      })
    }, 
    { immediate: true }
  )

  button.onclick = () => {
    flag.value = !flag.value
  }

  </script>
</body>

我們現在在監聽的時候綁一個監聽事件,onCleanup 接受一個 callback 函式,我們在函式中寫移除事件。

onCleanup 註冊的 callback 函式會在下一次 watch callback 即將執行**之前被呼叫,**這個時機確保了我們在新的副作用出現前,清理掉上一個過期的 Side Effect。

day25-01

你會發現回去點 app,它不會再被觸發了。

export function watch(source, cb, options) {
...
...
  let cleanup = null

  function onCleanup(cb) {
    cleanup = cb
  }

  function job() {

    if(cleanup) {
      // 確認是不是要清理之前的 sideEffect 函式
      cleanup()
      cleanup = null
    }

    // 執行 effect 的函式,得到新的數值,不能直接執行 getter,因為要收集依賴
    const newValue = effect.run()

    cb(newValue, oldValue, onCleanup)

    oldValue = newValue
  }
...
...
}

我們儲存外部傳入 onCleanup 的 callback 函式,把它儲存到變數之中,接著判斷,如果 onCleanup 傳入函式存在,在每次執行 job 函式之前,先執行一次清理函式。

onCleanupwatch API 中一個很重要但容易被忽略的特性。它為開發者提供了一個標準化的機制,來應對 Side Effect 帶來的挑戰:資源洩漏(如未移除的事件監聽器)與非同步競爭條件(如過期的網路請求)。


同步更新《嘿,日安!》技術部落格


上一篇
Day 24 - Watch:Options
下一篇
Day 26 - 陣列長度變更處理
系列文
從零到一打造 Vue3 響應式系統30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言