Skip to content

Commit 016ba11

Browse files
committed
fix(reactivity): fix iOS 12 JSON.stringify error on reactive objects
- Use WeakMap for raw -> reactive/readonly storage. This is slightly more expensive than using a field on the taget object but avoids polluting the original. - also fix Collection.forEach callback value fix #1916
1 parent 410e7ab commit 016ba11

File tree

4 files changed

+33
-30
lines changed

4 files changed

+33
-30
lines changed

packages/reactivity/__tests__/readonly.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ describe('reactivity/readonly', () => {
228228
test('should retrieve readonly values on iteration', () => {
229229
const key1 = {}
230230
const key2 = {}
231-
const original = new Collection([[key1, {}], [key2, {}]])
231+
const original = new Map([[key1, {}], [key2, {}]])
232232
const wrapped: any = readonly(original)
233233
expect(wrapped.size).toBe(2)
234234
for (const [key, value] of wrapped) {
@@ -246,7 +246,7 @@ describe('reactivity/readonly', () => {
246246
test('should retrieve reactive + readonly values on iteration', () => {
247247
const key1 = {}
248248
const key2 = {}
249-
const original = reactive(new Collection([[key1, {}], [key2, {}]]))
249+
const original = reactive(new Map([[key1, {}], [key2, {}]]))
250250
const wrapped: any = readonly(original)
251251
expect(wrapped.size).toBe(2)
252252
for (const [key, value] of wrapped) {

packages/reactivity/src/baseHandlers.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { reactive, readonly, toRaw, ReactiveFlags, Target } from './reactive'
1+
import {
2+
reactive,
3+
readonly,
4+
toRaw,
5+
ReactiveFlags,
6+
Target,
7+
readonlyMap,
8+
reactiveMap
9+
} from './reactive'
210
import { TrackOpTypes, TriggerOpTypes } from './operations'
311
import { track, trigger, ITERATE_KEY } from './effect'
412
import {
@@ -48,10 +56,7 @@ function createGetter(isReadonly = false, shallow = false) {
4856
return isReadonly
4957
} else if (
5058
key === ReactiveFlags.RAW &&
51-
receiver ===
52-
(isReadonly
53-
? target[ReactiveFlags.READONLY]
54-
: target[ReactiveFlags.REACTIVE])
59+
receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
5560
) {
5661
return target
5762
}

packages/reactivity/src/collectionHandlers.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -145,17 +145,17 @@ function createForEach(isReadonly: boolean, isShallow: boolean) {
145145
callback: Function,
146146
thisArg?: unknown
147147
) {
148-
const observed = this
149-
const target = toRaw(observed)
148+
const observed = this as any
149+
const target = observed[ReactiveFlags.RAW]
150+
const rawTarget = toRaw(target)
150151
const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
151-
!isReadonly && track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
152-
// important: create sure the callback is
153-
// 1. invoked with the reactive map as `this` and 3rd arg
154-
// 2. the value received should be a corresponding reactive/readonly.
155-
function wrappedCallback(value: unknown, key: unknown) {
152+
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
153+
return target.forEach((value: unknown, key: unknown) => {
154+
// important: make sure the callback is
155+
// 1. invoked with the reactive map as `this` and 3rd arg
156+
// 2. the value received should be a corresponding reactive/readonly.
156157
return callback.call(thisArg, wrap(value), wrap(key), observed)
157-
}
158-
return getProto(target).forEach.call(target, wrappedCallback)
158+
})
159159
}
160160
}
161161

packages/reactivity/src/reactive.ts

+12-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isObject, toRawType, def, hasOwn } from '@vue/shared'
1+
import { isObject, toRawType, def } from '@vue/shared'
22
import {
33
mutableHandlers,
44
readonlyHandlers,
@@ -16,20 +16,19 @@ export const enum ReactiveFlags {
1616
SKIP = '__v_skip',
1717
IS_REACTIVE = '__v_isReactive',
1818
IS_READONLY = '__v_isReadonly',
19-
RAW = '__v_raw',
20-
REACTIVE = '__v_reactive',
21-
READONLY = '__v_readonly'
19+
RAW = '__v_raw'
2220
}
2321

2422
export interface Target {
2523
[ReactiveFlags.SKIP]?: boolean
2624
[ReactiveFlags.IS_REACTIVE]?: boolean
2725
[ReactiveFlags.IS_READONLY]?: boolean
2826
[ReactiveFlags.RAW]?: any
29-
[ReactiveFlags.REACTIVE]?: any
30-
[ReactiveFlags.READONLY]?: any
3127
}
3228

29+
export const reactiveMap = new WeakMap<Target, any>()
30+
export const readonlyMap = new WeakMap<Target, any>()
31+
3332
const enum TargetType {
3433
INVALID = 0,
3534
COMMON = 1,
@@ -155,23 +154,22 @@ function createReactiveObject(
155154
return target
156155
}
157156
// target already has corresponding Proxy
158-
const reactiveFlag = isReadonly
159-
? ReactiveFlags.READONLY
160-
: ReactiveFlags.REACTIVE
161-
if (hasOwn(target, reactiveFlag)) {
162-
return target[reactiveFlag]
157+
const proxyMap = isReadonly ? readonlyMap : reactiveMap
158+
const existingProxy = proxyMap.get(target)
159+
if (existingProxy) {
160+
return existingProxy
163161
}
164162
// only a whitelist of value types can be observed.
165163
const targetType = getTargetType(target)
166164
if (targetType === TargetType.INVALID) {
167165
return target
168166
}
169-
const observed = new Proxy(
167+
const proxy = new Proxy(
170168
target,
171169
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
172170
)
173-
def(target, reactiveFlag, observed)
174-
return observed
171+
proxyMap.set(target, proxy)
172+
return proxy
175173
}
176174

177175
export function isReactive(value: unknown): boolean {

0 commit comments

Comments
 (0)