iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
Vue.js

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

Day 26 - 陣列長度變更處理

  • 分享至 

  • xImage
  •  

banner

在我們建構響應式系統的過程中,雖然對於原生 JavaScript 物件的處理已經算蠻完善,但陣列 (Array) 與普通物件的屬性不同,陣列的 length 屬性與其數值索引之間有緊密的聯動關係。

手動變更陣列長度

最直接改變陣列長度的方式就是手動賦值 。雖然這在日常開發中不被鼓勵,但一個健全的響應式系統必須能正確處理這種情況 。

<body>
  <div id="app"></div>
  <script type="module">
    // import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    import { reactive, effect } from '../dist/reactivity.esm.js'

    const state = reactive(['a', 'b', 'c','d'])

    effect(() => {
      console.log(state.length)
    })

    setTimeout(() => {
      state.length = 2
    }, 1000)
  </script>
</body>

在上述範例中,我們直接修改了陣列長度,它會觸發更新(通常我們會避免直接更改陣列長度的做法)。

像這樣直接更改陣列長度,多餘長度的數值會被刪除,如下圖:

day26-01

<body>
  <div id="app"></div>
  <script type="module">
    // import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    import { reactive, effect } from '../dist/reactivity.esm.js'

    const state = reactive(['a', 'b', 'c','d'])

    effect(() => {
      console.log(state[3])
    })

    setTimeout(() => {
      state.length = 2
    }, 1000)
  </script>
</body>

執行這段程式碼,控制台會先輸出 d,這符合預期。然而,一秒後當state.length 被修改為 2 時,console.log 並沒有再次執行 。

day26-02

然而,問題出在哪裡?

我們的 effect 依賴的是 state[3]。當 state.length 被修改為 2 時,索引為 3 的元素實際上已經被刪除了。

因此,這個 effect 所依賴的 key ('3') 後續不會再發生任何 set 行為,導致它再也沒有機會被重新觸發。依賴關係因此遺失。

在進行「刪除」操作,僅觸發了 state 物件 length 屬性的 set並未觸發索引 '3'set

所以我們需要做的是,當 length 被短時,我們要找出所有依賴「被刪除索引」的 effect,並通知它們重新執行 :

//dep.ts
export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  // 如果 depsMap 不存在,表示沒有收集過依賴,直接返回
  if (!depsMap) return

  const targetIsArray = Array.isArray(target)

  if(targetIsArray && key === 'length') {

    depsMap.forEach((dep, depKey) => {
      if(depKey >=length || depKey === 'length') {
        // 通知訪問大於等於 length 的 effect 以及 訪問了 length 的 effect 重新執行
        propagate(dep.subs)
      }
    })
  }else{
    // 如果不是陣列,並且更新的不是length,則直接取得依賴
    const dep = depsMap.get(key)
    // 如果依賴不存在,表示這個 key 沒有在effect中被使用過,直接返回
    if (!dep) return

    // 找到依賴,觸發更新
    propagate(dep.subs)
  }

}

state.length = 2 時,effect 會被重新觸發,現在看我們的範例程式碼,會發現觸發更新結果是 undefined,因為state[3]不存在。

day26-03

陣列方法導致長度變更

會影響陣列長度除了直接賦值之外,像是我們常用的陣列方法:poppushshift 等等,都會隱性地影響到陣列長度:

<body>
  <div id="app"></div>
  <script type="module">
    // import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    import { reactive, effect } from '../dist/reactivity.esm.js'

    const state = reactive(['a', 'b', 'c','d'])

    effect(() => {
      console.log(state.length)
    })

    setTimeout(() => {
      state.push('e')
    }, 1000)
  </script>
</body>

這邊可以看到我增加了一個索引,但是依賴 lengtheffect 並沒有觸發更新:

day26-04

那麼,當 push 這類方法被呼叫時,我們如何偵測到 length 的隱性變更呢?

關鍵在於攔截 set 操作。push('e') 的底層操作,除了在索引 4 上設置新值,也會修改 length 屬性。

我們可以在 set 代理中,比較操作前後的陣列長度,如果不一致,就主動觸發 length 屬性的依賴更新:

//baseHandlers.ts

export const mutableHandlers = {
  ...
  ...
  set(target, key, newValue, receiver) {
    const oldValue = target[key]
    const targetIsArray = Array.isArray(target)
    // 如果 target 是陣列,取得其舊長度
    const oldLength = targetIsArray ? target.length : 0
    const res = Reflect.set(target, key, newValue, receiver)
    if(isRef(oldValue) && !isRef(newValue)){
      oldValue.value = newValue

      // 改了 ref 的值,會通知 sub 更新
      // 所以要 return 不然下方 trigger 又會觸發 trigger 更新 會觸發兩次
      return res
    }
    if(hasChanged(newValue, oldValue)){
      // 如果舊值不等於新值,則觸發更新
      // 觸發更新:通知之前收集的依賴,重新執行effect
      trigger(target, key)
    }
    
		// 如果 target 是陣列,取得其新長度
    const newLength = targetIsArray ? newValue.length : 0

    // 如果 target 是陣列,並且新長度不等於舊長度,並且 key 不是 length,則觸發更新
    if(targetIsArray && newLength !== oldLength && key !== 'length'){
      trigger(target, 'length')
    }
    return res
  }
}

在觸發更新前,我們取得新值跟舊值:

  • 當依賴是陣列類型
  • 更新前的陣列長度跟更新後陣列長度不同
  • 並且 key 不是 length,就觸發更新:避免重複觸發,因為我們剛剛在 trigger 函式已經寫了觸發更新。

今天我們聚焦於陣列 length 屬性的特殊性。透過分別在 trigger 函式和 set 代理中增加特殊的處理邏輯:

  • trigger:處理了手動縮短 length 時,對已刪除索引的依賴觸發。
  • set:處理了陣列方法隱性改變 length 時,對 length 屬性的依賴觸發。

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


上一篇
Day 25 - Watch :清理 SideEffect
下一篇
Day 27 - toRef、toRefs、ProxyRef、unref
系列文
從零到一打造 Vue3 響應式系統30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言