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, getRestElement,
renderEffect, renderEffect,
} from '../src' } 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' import { makeRender } from './_utils'
const define = makeRender() const define = makeRender()
@ -674,4 +681,57 @@ describe('createFor', () => {
await nextTick() await nextTick()
expectCalledTimesToBe('Clear rows', 1, 0, 0, 0) 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, EffectScope,
type ShallowRef, type ShallowRef,
isReactive, isReactive,
isReadonly,
isShallow, isShallow,
pauseTracking, pauseTracking,
resetTracking, resetTracking,
shallowReadArray, shallowReadArray,
shallowRef, shallowRef,
toReactive, toReactive,
toReadonly,
} from '@vue/reactivity' } from '@vue/reactivity'
import { getSequence, isArray, isObject, isString } from '@vue/shared' import { getSequence, isArray, isObject, isString } from '@vue/shared'
import { createComment, createTextNode } from './dom/node' import { createComment, createTextNode } from './dom/node'
@ -59,6 +61,7 @@ type Source = any[] | Record<any, any> | number | Set<any> | Map<any, any>
type ResolvedSource = { type ResolvedSource = {
values: any[] values: any[]
needsWrap: boolean needsWrap: boolean
isReadonlySource: boolean
keys?: string[] keys?: string[]
} }
@ -393,11 +396,13 @@ export function createForSlots(
function normalizeSource(source: any): ResolvedSource { function normalizeSource(source: any): ResolvedSource {
let values = source let values = source
let needsWrap = false let needsWrap = false
let isReadonlySource = false
let keys let keys
if (isArray(source)) { if (isArray(source)) {
if (isReactive(source)) { if (isReactive(source)) {
needsWrap = !isShallow(source) needsWrap = !isShallow(source)
values = shallowReadArray(source) values = shallowReadArray(source)
isReadonlySource = isReadonly(source)
} }
} else if (isString(source)) { } else if (isString(source)) {
values = source.split('') values = source.split('')
@ -418,14 +423,23 @@ function normalizeSource(source: any): ResolvedSource {
} }
} }
} }
return { values, needsWrap, keys } return {
values,
needsWrap,
isReadonlySource,
keys,
}
} }
function getItem( function getItem(
{ keys, values, needsWrap }: ResolvedSource, { keys, values, needsWrap, isReadonlySource }: ResolvedSource,
idx: number, idx: number,
): [item: any, key: any, index?: 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) { if (keys) {
return [value, keys[idx], idx] return [value, keys[idx], idx]
} else { } else {