fix(runtime-core): respect immutability for readonly reactive arrays in `v-for` (#13091)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details
size data / upload (push) Has been cancelled Details

close #13087
This commit is contained in:
Tycho 2025-05-02 17:55:24 +08:00 committed by GitHub
parent 9196222ae1
commit 3f27c58ffb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 43 additions and 2 deletions

View 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' import { renderList } from '../../src/helpers/renderList'
describe('renderList', () => { describe('renderList', () => {
@ -65,4 +71,31 @@ describe('renderList', () => {
const shallowReactiveArray = shallowReactive([{ foo: 1 }]) const shallowReactiveArray = shallowReactive([{ foo: 1 }])
expect(renderList(shallowReactiveArray, isReactive)).toEqual([false]) 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])
})
}) })

View File

@ -1,9 +1,11 @@
import type { VNode, VNodeChild } from '../vnode' import type { VNode, VNodeChild } from '../vnode'
import { import {
isReactive, isReactive,
isReadonly,
isShallow, isShallow,
shallowReadArray, shallowReadArray,
toReactive, toReactive,
toReadonly,
} from '@vue/reactivity' } from '@vue/reactivity'
import { isArray, isObject, isString } from '@vue/shared' import { isArray, isObject, isString } from '@vue/shared'
import { warn } from '../warning' import { warn } from '../warning'
@ -69,14 +71,20 @@ export function renderList(
if (sourceIsArray || isString(source)) { if (sourceIsArray || isString(source)) {
const sourceIsReactiveArray = sourceIsArray && isReactive(source) const sourceIsReactiveArray = sourceIsArray && isReactive(source)
let needsWrap = false let needsWrap = false
let isReadonlySource = false
if (sourceIsReactiveArray) { if (sourceIsReactiveArray) {
needsWrap = !isShallow(source) needsWrap = !isShallow(source)
isReadonlySource = isReadonly(source)
source = shallowReadArray(source) source = shallowReadArray(source)
} }
ret = new Array(source.length) ret = new Array(source.length)
for (let i = 0, l = source.length; i < l; i++) { for (let i = 0, l = source.length; i < l; i++) {
ret[i] = renderItem( ret[i] = renderItem(
needsWrap ? toReactive(source[i]) : source[i], needsWrap
? isReadonlySource
? toReadonly(toReactive(source[i]))
: toReactive(source[i])
: source[i],
i, i,
undefined, undefined,
cached && cached[i], cached && cached[i],