fix(runtime-vapor): respect immutability for readonly reactive arrays in `v-for` (#13187)

This commit is contained in:
Tycho 2025-06-18 10:17:22 +08:00 committed by GitHub
parent a0c42ffbbc
commit 7d84010c0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 78 additions and 4 deletions

View 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-->')
})
})
})

View 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 {