]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-core): respect immutability for readonly reactive arrays in `v-for` ...
authorTycho <jh.leong@outlook.com>
Fri, 2 May 2025 09:55:24 +0000 (17:55 +0800)
committerGitHub <noreply@github.com>
Fri, 2 May 2025 09:55:24 +0000 (02:55 -0700)
close #13087

packages/runtime-core/__tests__/helpers/renderList.spec.ts
packages/runtime-core/src/helpers/renderList.ts

index 8dd04ff8a9b4d5a7e7eaf5165f093885e6ff1468..6e766f1a169ce9915f9a89e1236c84e44a7877b9 100644 (file)
@@ -1,4 +1,10 @@
-import { isReactive, reactive, shallowReactive } from '../../src/index'
+import {
+  effect,
+  isReactive,
+  reactive,
+  readonly,
+  shallowReactive,
+} from '../../src/index'
 import { renderList } from '../../src/helpers/renderList'
 
 describe('renderList', () => {
@@ -65,4 +71,31 @@ describe('renderList', () => {
     const shallowReactiveArray = shallowReactive([{ foo: 1 }])
     expect(renderList(shallowReactiveArray, isReactive)).toEqual([false])
   })
+
+  it('should not allow mutation', () => {
+    const arr = readonly(reactive([{ foo: 1 }]))
+    expect(
+      renderList(arr, item => {
+        ;(item as any).foo = 0
+        return item.foo
+      }),
+    ).toEqual([1])
+    expect(
+      `Set operation on key "foo" failed: target is readonly.`,
+    ).toHaveBeenWarned()
+  })
+
+  it('should trigger effect for deep mutations in readonly reactive arrays', () => {
+    const arr = reactive([{ foo: 1 }])
+    const readonlyArr = readonly(arr)
+
+    let dummy
+    effect(() => {
+      dummy = renderList(readonlyArr, item => item.foo)
+    })
+    expect(dummy).toEqual([1])
+
+    arr[0].foo = 2
+    expect(dummy).toEqual([2])
+  })
 })
index bbcbcc13044330fed58c776f684ece2ff837c0b5..8ffe29edf68f3a5d4623426073aa25b1dc8b752b 100644 (file)
@@ -1,9 +1,11 @@
 import type { VNode, VNodeChild } from '../vnode'
 import {
   isReactive,
+  isReadonly,
   isShallow,
   shallowReadArray,
   toReactive,
+  toReadonly,
 } from '@vue/reactivity'
 import { isArray, isObject, isString } from '@vue/shared'
 import { warn } from '../warning'
@@ -69,14 +71,20 @@ export function renderList(
   if (sourceIsArray || isString(source)) {
     const sourceIsReactiveArray = sourceIsArray && isReactive(source)
     let needsWrap = false
+    let isReadonlySource = false
     if (sourceIsReactiveArray) {
       needsWrap = !isShallow(source)
+      isReadonlySource = isReadonly(source)
       source = shallowReadArray(source)
     }
     ret = new Array(source.length)
     for (let i = 0, l = source.length; i < l; i++) {
       ret[i] = renderItem(
-        needsWrap ? toReactive(source[i]) : source[i],
+        needsWrap
+          ? isReadonlySource
+            ? toReadonly(toReactive(source[i]))
+            : toReactive(source[i])
+          : source[i],
         i,
         undefined,
         cached && cached[i],