]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-vapor): respect immutability for readonly reactive arrays in `v-for`...
authorTycho <jh.leong@outlook.com>
Wed, 18 Jun 2025 02:17:22 +0000 (10:17 +0800)
committerGitHub <noreply@github.com>
Wed, 18 Jun 2025 02:17:22 +0000 (10:17 +0800)
packages/runtime-vapor/__tests__/for.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts

index 7ba6023b1e97a2d1a9945d175111413fd53c10d6..db91b6a62da9aa0cb7e172348d0b210b920faafa 100644 (file)
@@ -4,7 +4,14 @@ import {
   getRestElement,
   renderEffect,
 } from '../src'
-import { nextTick, ref, shallowRef, triggerRef } from '@vue/runtime-dom'
+import {
+  nextTick,
+  reactive,
+  readonly,
+  ref,
+  shallowRef,
+  triggerRef,
+} from '@vue/runtime-dom'
 import { makeRender } from './_utils'
 
 const define = makeRender()
@@ -674,4 +681,57 @@ describe('createFor', () => {
     await nextTick()
     expectCalledTimesToBe('Clear rows', 1, 0, 0, 0)
   })
+
+  describe('readonly source', () => {
+    test('should not allow mutation', () => {
+      const arr = readonly(reactive([{ foo: 1 }]))
+
+      const { host } = define(() => {
+        const n1 = createFor(
+          () => arr,
+          (item, key, index) => {
+            const span = document.createElement('li')
+            renderEffect(() => {
+              item.value.foo = 0
+              span.innerHTML = `${item.value.foo}`
+            })
+            return span
+          },
+          idx => idx,
+        )
+        return n1
+      }).render()
+
+      expect(host.innerHTML).toBe('<li>1</li><!--for-->')
+      expect(
+        `Set operation on key "foo" failed: target is readonly.`,
+      ).toHaveBeenWarned()
+    })
+
+    test('should trigger effect for deep mutations', async () => {
+      const arr = reactive([{ foo: 1 }])
+      const readonlyArr = readonly(arr)
+
+      const { host } = define(() => {
+        const n1 = createFor(
+          () => readonlyArr,
+          (item, key, index) => {
+            const span = document.createElement('li')
+            renderEffect(() => {
+              span.innerHTML = `${item.value.foo}`
+            })
+            return span
+          },
+          idx => idx,
+        )
+        return n1
+      }).render()
+
+      expect(host.innerHTML).toBe('<li>1</li><!--for-->')
+
+      arr[0].foo = 2
+      await nextTick()
+      expect(host.innerHTML).toBe('<li>2</li><!--for-->')
+    })
+  })
 })
index e75ca1f3ea878b9f5a1b5c7307704668781582df..62529149ad4ce09256fe2f3d931f1541b86c336e 100644 (file)
@@ -2,12 +2,14 @@ import {
   EffectScope,
   type ShallowRef,
   isReactive,
+  isReadonly,
   isShallow,
   pauseTracking,
   resetTracking,
   shallowReadArray,
   shallowRef,
   toReactive,
+  toReadonly,
 } from '@vue/reactivity'
 import { getSequence, isArray, isObject, isString } from '@vue/shared'
 import { createComment, createTextNode } from './dom/node'
@@ -59,6 +61,7 @@ type Source = any[] | Record<any, any> | number | Set<any> | Map<any, any>
 type ResolvedSource = {
   values: any[]
   needsWrap: boolean
+  isReadonlySource: boolean
   keys?: string[]
 }
 
@@ -393,11 +396,13 @@ export function createForSlots(
 function normalizeSource(source: any): ResolvedSource {
   let values = source
   let needsWrap = false
+  let isReadonlySource = false
   let keys
   if (isArray(source)) {
     if (isReactive(source)) {
       needsWrap = !isShallow(source)
       values = shallowReadArray(source)
+      isReadonlySource = isReadonly(source)
     }
   } else if (isString(source)) {
     values = source.split('')
@@ -418,14 +423,23 @@ function normalizeSource(source: any): ResolvedSource {
       }
     }
   }
-  return { values, needsWrap, keys }
+  return {
+    values,
+    needsWrap,
+    isReadonlySource,
+    keys,
+  }
 }
 
 function getItem(
-  { keys, values, needsWrap }: ResolvedSource,
+  { keys, values, needsWrap, isReadonlySource }: ResolvedSource,
   idx: number,
 ): [item: any, key: any, index?: number] {
-  const value = needsWrap ? toReactive(values[idx]) : values[idx]
+  const value = needsWrap
+    ? isReadonlySource
+      ? toReadonly(toReactive(values[idx]))
+      : toReactive(values[idx])
+    : values[idx]
   if (keys) {
     return [value, keys[idx], idx]
   } else {