為什麼 .reverse() 讓瀏覽器凍結?Vue Reactivity 與 Array In-Place Mutation 的陷阱
在一個 Vue 3 專案中,我把一行 .sort() 改成 .reverse(),結果瀏覽器直接凍結。這篇文章記錄這個問題的根本原因:在 reactive context 中對陣列做 in-place mutation 可能觸發無限迴圈,以及為什麼 ES2023 的 toReversed() / toSorted() 是更安全的選擇。
問題現場
一個 Chart.js 圖表模組,buildChartConfig 負責把 API 回傳的資料轉成 chart 設定。原本用 .sort() 按 id 降序排列,運作正常:
// 正常運作
const items = chartData.category_analysis.sort((a, b) => b.id - a.id)
因為資料本身已經按 id 由小到大排好,所以改成語意更清楚的 .reverse():
// 瀏覽器凍結
const items = chartData.category_analysis.reverse()
改完之後,打開頁面瀏覽器直接凍結,看起來像是 OOM 或 memory leak。
追蹤問題根源
buildChartConfig 本身只是一個普通 function,不在 computed 裡。但往上追 call chain:
// composable
function useChart(data: { name: string, visual_data: VisualData }) {
function drawCharts() {
for (const section of allSections) {
const config = section.buildChartConfig({
visualData: data.visual_data, // ← reactive 物件的 reference
// ...
})
new Chart(ctx, config)
}
}
return { drawCharts }
}
在使用這個 composable 的元件裡:
const { drawCharts } = useChart(props.data)
watch(props.data, () => {
nextTick(() => {
drawCharts()
})
}, { deep: true })
問題的完整路徑:
watch(props.data, { deep: true })監聽 props 變化- 觸發
drawCharts()→buildChartConfig() chartData.category_analysis.reverse()直接 mutate 了 reactive 陣列- Vue 的 Proxy 攔截到 mutation → 通知 watcher
- watcher 偵測到
props.data內部變化 → 又觸發drawCharts() - 又
reverse()→ 又 mutate → 又觸發 watch → 無限迴圈
為什麼 .sort() 沒事?
.sort() 和 .reverse() 都是 in-place mutation method1,都會直接修改原陣列。差別在於操作結果是否「收斂」:
.sort((a, b) => b.id - a.id) — 第一次排完是 [3, 2, 1],watch 觸發後再 sort 一次,結果還是 [3, 2, 1]。Vue 比對後發現陣列沒變,不再觸發 update。迴圈在第二次就停了。
.reverse() — 第一次翻轉成 [3, 2, 1],watch 觸發後再 reverse 變成 [1, 2, 3],下一次又變 [3, 2, 1]… 永遠在兩個狀態之間來回切換,Vue 每次都偵測到變化,永遠不會停。
所以 .sort() 不是「沒有 bug」,它只是剛好收斂了。本質上一樣是在 reactive context 裡 mutate source data,只是運氣好沒爆。
Vue 3 Reactivity 與 Array Mutation
Vue 3 使用 Proxy 實作 reactivity system2。當你對一個 reactive 物件(包括陣列)進行操作時,Proxy 會攔截 get、set 等操作,自動追蹤依賴和觸發更新。
對陣列來說,Vue 會攔截所有的 mutation method(push、pop、splice、sort、reverse 等)。當這些方法被呼叫時,Vue 會通知所有依賴這個陣列的 watcher 和 computed。
搭配 watch(source, callback, { deep: true }),Vue 會遞迴追蹤 source 內部所有屬性的變化3。這意味著即使你 mutate 的是 props.data.visual_data.category_analysis(一個深層巢狀的陣列),最外層的 watch(props.data) 也會被觸發。
這就是為什麼一個看似無害的 .reverse() 能造成無限迴圈 — 它 mutate 了 reactive source 的深層屬性,而 deep watcher 會捕捉到這個變化。
解法:使用 Non-Mutating 方法
ES2023 引入了四個 non-mutating 的陣列方法4:
| Mutating(原地修改) | Non-Mutating(回傳新陣列) |
|---|---|
sort() | toSorted() |
reverse() | toReversed() |
splice() | toSpliced() |
arr[i] = value | with(i, value) |
修正方式:
// ✅ 不 mutate 原陣列,回傳新陣列
const items = chartData.category_analysis.toReversed()
toReversed() 回傳一個全新的陣列,完全不碰原本的 reactive proxy。原陣列沒被 mutate,watch 就不會被觸發,迴圈根本不會開始。
如果環境不支援 ES2023,用 spread 展開再操作也可以:
// ES2023 之前的寫法
const items = [...chartData.category_analysis].reverse()
[...arr] 產生新陣列(shallow copy),再對新陣列 .reverse(),原陣列不受影響。
Shallow Copy 夠用嗎?
toReversed() 和 [...arr].reverse() 都是 shallow copy — 陣列容器是新的,但裡面的元素還是同一批 reference:
const original = [objA, objB, objC]
const reversed = original.toReversed()
reversed === original // false(新陣列)
reversed[0] === original[2] // true(同一個物件 reference)
在這個場景下 shallow copy 就夠了。問題出在「陣列本身的順序被改了」觸發 watch,不是裡面的物件內容被改。而且 buildChartConfig 只是讀取資料來建 chart config,沒有寫入元素屬性,所以不存在穿透回 reactive source 的風險。
不需要 structuredClone 或 JSON.parse(JSON.stringify()),那是 deep clone,對一個只需要反轉順序的 read-only 場景來說完全沒必要。
判斷原則:什麼時候該用 Non-Mutating 方法?
一個簡單的判斷:如果你操作的陣列來源是 reactive 的(ref、reactive、props、computed 的回傳值),就應該用 non-mutating 方法。
安全的情境(不需要改):
// .filter() 已經產生新陣列,後面的 .sort() 不會 mutate 原始資料
const result = props.options.filter(o => o.active).sort((a, b) => a.name.localeCompare(b.name))
// .map() 已經產生新陣列
const labels = data.map(d => d.label).sort()
// Object.entries() 產生新陣列
const sorted = Object.entries(grouped).sort(([a], [b]) => b.localeCompare(a))
需要改的情境:
// ❌ 直接 mutate reactive 陣列
const items = reactiveArray.sort((a, b) => a.id - b.id)
const items = reactiveArray.reverse()
// ✅ 改用 non-mutating
const items = reactiveArray.toSorted((a, b) => a.id - b.id)
const items = reactiveArray.toReversed()
關鍵是看 .sort() / .reverse() 前面有沒有一個會產生新陣列的操作(.filter()、.map()、[...arr]、Object.entries() 等)。如果有,就是安全的;如果沒有,就是直接 mutate reactive source。
環境資訊
- Vue: 3.x
- Nuxt: 3.x(SPA mode,
ssr: false) - Chart.js: 4.x
- ES2023
toReversed()/toSorted()— 所有主流瀏覽器自 2023 年起支援5
本文撰寫時間:2026 年 3 月,技術版本可能隨時間更新,請以官方文件為準。