From 6fca482004d45387dfd5326d1b9ed5004b228816 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 20 Nov 2025 08:49:05 +0800 Subject: [PATCH] fix(reactivity): correctly wrap iterated array items to preserve their readonly status --- .../reactivity/__tests__/readonly.spec.ts | 13 +++++ .../reactivity/src/arrayInstrumentations.ts | 54 +++++++++++++++---- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts index a6f7542ce12..96e688e38e0 100644 --- a/packages/reactivity/__tests__/readonly.spec.ts +++ b/packages/reactivity/__tests__/readonly.spec.ts @@ -172,6 +172,19 @@ describe('reactivity/readonly', () => { expect(dummy).toBe(1) expect(`target is readonly`).toHaveBeenWarnedTimes(2) }) + + it('should maintain identity when iterating readonly ref array', () => { + const list = readonly(ref([{}, {}, {}])) + const computedList = computed(() => { + const newList: any[] = [] + list.value.forEach(x => newList.push(x)) + return newList + }) + + expect(list.value[0]).toBe(computedList.value[0]) + expect(isReadonly(computedList.value[0])).toBe(true) + expect(isReactive(computedList.value[0])).toBe(true) + }) }) const maps = [Map, WeakMap] diff --git a/packages/reactivity/src/arrayInstrumentations.ts b/packages/reactivity/src/arrayInstrumentations.ts index 981a10d04fc..f37b056516a 100644 --- a/packages/reactivity/src/arrayInstrumentations.ts +++ b/packages/reactivity/src/arrayInstrumentations.ts @@ -1,6 +1,14 @@ import { TrackOpTypes } from './constants' import { endBatch, pauseTracking, resetTracking, startBatch } from './effect' -import { isProxy, isShallow, toRaw, toReactive } from './reactive' +import { + isProxy, + isReactive, + isReadonly, + isShallow, + toRaw, + toReactive, + toReadonly, +} from './reactive' import { ARRAY_ITERATE_KEY, track } from './dep' import { isArray } from '@vue/shared' @@ -24,11 +32,18 @@ export function shallowReadArray(arr: T[]): T[] { return arr } +function toWrapped(target: unknown, item: unknown) { + if (isReadonly(target)) { + return isReactive(target) ? toReadonly(toReactive(item)) : toReadonly(item) + } + return toReactive(item) +} + export const arrayInstrumentations: Record = { __proto__: null, [Symbol.iterator]() { - return iterator(this, Symbol.iterator, toReactive) + return iterator(this, Symbol.iterator, item => toWrapped(this, item)) }, concat(...args: unknown[]) { @@ -39,7 +54,7 @@ export const arrayInstrumentations: Record = { entries() { return iterator(this, 'entries', (value: [number, unknown]) => { - value[1] = toReactive(value[1]) + value[1] = toWrapped(this, value[1]) return value }) }, @@ -55,14 +70,28 @@ export const arrayInstrumentations: Record = { fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, ) { - return apply(this, 'filter', fn, thisArg, v => v.map(toReactive), arguments) + return apply( + this, + 'filter', + fn, + thisArg, + v => v.map((item: unknown) => toWrapped(this, item)), + arguments, + ) }, find( fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown, ) { - return apply(this, 'find', fn, thisArg, toReactive, arguments) + return apply( + this, + 'find', + fn, + thisArg, + item => toWrapped(this, item), + arguments, + ) }, findIndex( @@ -76,7 +105,14 @@ export const arrayInstrumentations: Record = { fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown, ) { - return apply(this, 'findLast', fn, thisArg, toReactive, arguments) + return apply( + this, + 'findLast', + fn, + thisArg, + item => toWrapped(this, item), + arguments, + ) }, findLastIndex( @@ -189,7 +225,7 @@ export const arrayInstrumentations: Record = { }, values() { - return iterator(this, 'values', toReactive) + return iterator(this, 'values', item => toWrapped(this, item)) }, } @@ -257,7 +293,7 @@ function apply( if (arr !== self) { if (needsWrap) { wrappedFn = function (this: unknown, item, index) { - return fn.call(this, toReactive(item), index, self) + return fn.call(this, toWrapped(self, item), index, self) } } else if (fn.length > 2) { wrappedFn = function (this: unknown, item, index) { @@ -281,7 +317,7 @@ function reduce( if (arr !== self) { if (!isShallow(self)) { wrappedFn = function (this: unknown, acc, item, index) { - return fn.call(this, acc, toReactive(item), index, self) + return fn.call(this, acc, toWrapped(self, item), index, self) } } else if (fn.length > 3) { wrappedFn = function (this: unknown, acc, item, index) {