嘿,日安!

Day 26 - 陣列長度變更處理

5th October 2025
Front-end
Vue3
IT鐵人賽2025
從零到一打造 Vue3 響應式系統
Last updated:9th October 2025
7 Minutes
1236 Words

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

手動變更陣列長度

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

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 { reactive, effect } from '../dist/reactivity.esm.js'
6
7
const state = reactive(['a', 'b', 'c','d'])
8
9
effect(() => {
10
console.log(state.length)
11
})
12
13
setTimeout(() => {
14
state.length = 2
15
}, 1000)
2 collapsed lines
16
</script>
17
</body>

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

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

default

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 { reactive, effect } from '../dist/reactivity.esm.js'
6
7
const state = reactive(['a', 'b', 'c','d'])
8
9
effect(() => {
10
console.log(state[3])
11
})
12
13
setTimeout(() => {
14
state.length = 2
15
}, 1000)
2 collapsed lines
16
</script>
17
</body>

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

default

然而,問題出在哪裡?

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

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

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

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

dep.ts
1
export function trigger(target, key) {
2
const depsMap = targetMap.get(target)
3
// 如果 depsMap 不存在,表示沒有收集過依賴,直接返回
4
if (!depsMap) return
5
6
const targetIsArray = Array.isArray(target)
7
8
if(targetIsArray && key === 'length') {
9
10
depsMap.forEach((dep, depKey) => {
11
if(depKey >=length || depKey === 'length') {
12
// 通知訪問大於等於 length 的 effect 以及 訪問了 length 的 effect 重新執行
13
propagate(dep.subs)
14
}
12 collapsed lines
15
})
16
}else{
17
// 如果不是陣列,並且更新的不是length,則直接取得依賴
18
const dep = depsMap.get(key)
19
// 如果依賴不存在,表示這個 key 沒有在effect中被使用過,直接返回
20
if (!dep) return
21
22
// 找到依賴,觸發更新
23
propagate(dep.subs)
24
}
25
26
}

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

default

陣列方法導致長度變更

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

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 { reactive, effect } from '../dist/reactivity.esm.js'
6
7
const state = reactive(['a', 'b', 'c','d'])
8
9
effect(() => {
10
console.log(state.length)
11
})
12
13
setTimeout(() => {
14
state.push('e')
15
}, 1000)
2 collapsed lines
16
</script>
17
</body>

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

default

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

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

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

baseHandlers.ts
1
export const mutableHandlers = {
2
...
3
...
4
set(target, key, newValue, receiver) {
5
const oldValue = target[key]
6
const targetIsArray = Array.isArray(target)
7
// 如果 target 是陣列,取得其舊長度
8
const oldLength = targetIsArray ? target.length : 0
9
const res = Reflect.set(target, key, newValue, receiver)
10
if(isRef(oldValue) && !isRef(newValue)){
11
oldValue.value = newValue
12
13
// 改了 ref 的值,會通知 sub 更新
19 collapsed lines
14
// 所以要 return 不然下方 trigger 又會觸發 trigger 更新 會觸發兩次
15
return res
16
}
17
if(hasChanged(newValue, oldValue)){
18
// 如果舊值不等於新值,則觸發更新
19
// 觸發更新:通知之前收集的依賴,重新執行effect
20
trigger(target, key)
21
}
22
23
// 如果 target 是陣列,取得其新長度
24
const newLength = targetIsArray ? newValue.length : 0
25
26
// 如果 target 是陣列,並且新長度不等於舊長度,並且 key 不是 length,則觸發更新
27
if(targetIsArray && newLength !== oldLength && key !== 'length'){
28
trigger(target, 'length')
29
}
30
return res
31
}
32
}

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

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

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

  • trigger:處理了手動縮短 length 時,對已刪除索引的依賴觸發。
  • set:處理了陣列方法隱性改變 length 時,對 length 屬性的依賴觸發。
Article title:Day 26 - 陣列長度變更處理
Article author:日安
Release time:5th October 2025