diff --git a/packages/runtime-core/__tests__/helpers/renderList.spec.ts b/packages/runtime-core/__tests__/helpers/renderList.spec.ts index 8dd04ff8a9b..6e766f1a169 100644 --- a/packages/runtime-core/__tests__/helpers/renderList.spec.ts +++ b/packages/runtime-core/__tests__/helpers/renderList.spec.ts @@ -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]) + }) }) diff --git a/packages/runtime-core/src/helpers/renderList.ts b/packages/runtime-core/src/helpers/renderList.ts index bbcbcc13044..8ffe29edf68 100644 --- a/packages/runtime-core/src/helpers/renderList.ts +++ b/packages/runtime-core/src/helpers/renderList.ts @@ -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],