From 016ba116a8715d90858de4244073198958d735ff Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 24 Aug 2020 15:26:53 -0400 Subject: [PATCH] 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 --- .../reactivity/__tests__/readonly.spec.ts | 4 +-- packages/reactivity/src/baseHandlers.ts | 15 +++++++---- packages/reactivity/src/collectionHandlers.ts | 18 ++++++------- packages/reactivity/src/reactive.ts | 26 +++++++++---------- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts index 3c22ce2905e..ff73e43bcf7 100644 --- a/packages/reactivity/__tests__/readonly.spec.ts +++ b/packages/reactivity/__tests__/readonly.spec.ts @@ -228,7 +228,7 @@ describe('reactivity/readonly', () => { test('should retrieve readonly values on iteration', () => { const key1 = {} const key2 = {} - const original = new Collection([[key1, {}], [key2, {}]]) + const original = new Map([[key1, {}], [key2, {}]]) const wrapped: any = readonly(original) expect(wrapped.size).toBe(2) for (const [key, value] of wrapped) { @@ -246,7 +246,7 @@ describe('reactivity/readonly', () => { test('should retrieve reactive + readonly values on iteration', () => { const key1 = {} const key2 = {} - const original = reactive(new Collection([[key1, {}], [key2, {}]])) + const original = reactive(new Map([[key1, {}], [key2, {}]])) const wrapped: any = readonly(original) expect(wrapped.size).toBe(2) for (const [key, value] of wrapped) { diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 8d8112e926a..ef944a968a6 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -1,4 +1,12 @@ -import { reactive, readonly, toRaw, ReactiveFlags, Target } from './reactive' +import { + reactive, + readonly, + toRaw, + ReactiveFlags, + Target, + readonlyMap, + reactiveMap +} from './reactive' import { TrackOpTypes, TriggerOpTypes } from './operations' import { track, trigger, ITERATE_KEY } from './effect' import { @@ -48,10 +56,7 @@ function createGetter(isReadonly = false, shallow = false) { return isReadonly } else if ( key === ReactiveFlags.RAW && - receiver === - (isReadonly - ? target[ReactiveFlags.READONLY] - : target[ReactiveFlags.REACTIVE]) + receiver === (isReadonly ? readonlyMap : reactiveMap).get(target) ) { return target } diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 126c94460d1..4f4b9726140 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -145,17 +145,17 @@ function createForEach(isReadonly: boolean, isShallow: boolean) { callback: Function, thisArg?: unknown ) { - const observed = this - const target = toRaw(observed) + const observed = this as any + const target = observed[ReactiveFlags.RAW] + const rawTarget = toRaw(target) const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive - !isReadonly && track(target, TrackOpTypes.ITERATE, ITERATE_KEY) - // important: create sure the callback is - // 1. invoked with the reactive map as `this` and 3rd arg - // 2. the value received should be a corresponding reactive/readonly. - function wrappedCallback(value: unknown, key: unknown) { + !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY) + return target.forEach((value: unknown, key: unknown) => { + // important: make sure the callback is + // 1. invoked with the reactive map as `this` and 3rd arg + // 2. the value received should be a corresponding reactive/readonly. return callback.call(thisArg, wrap(value), wrap(key), observed) - } - return getProto(target).forEach.call(target, wrappedCallback) + }) } } diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index a6602db2c5c..3f6da8364a0 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -1,4 +1,4 @@ -import { isObject, toRawType, def, hasOwn } from '@vue/shared' +import { isObject, toRawType, def } from '@vue/shared' import { mutableHandlers, readonlyHandlers, @@ -16,9 +16,7 @@ export const enum ReactiveFlags { SKIP = '__v_skip', IS_REACTIVE = '__v_isReactive', IS_READONLY = '__v_isReadonly', - RAW = '__v_raw', - REACTIVE = '__v_reactive', - READONLY = '__v_readonly' + RAW = '__v_raw' } export interface Target { @@ -26,10 +24,11 @@ export interface Target { [ReactiveFlags.IS_REACTIVE]?: boolean [ReactiveFlags.IS_READONLY]?: boolean [ReactiveFlags.RAW]?: any - [ReactiveFlags.REACTIVE]?: any - [ReactiveFlags.READONLY]?: any } +export const reactiveMap = new WeakMap() +export const readonlyMap = new WeakMap() + const enum TargetType { INVALID = 0, COMMON = 1, @@ -155,23 +154,22 @@ function createReactiveObject( return target } // target already has corresponding Proxy - const reactiveFlag = isReadonly - ? ReactiveFlags.READONLY - : ReactiveFlags.REACTIVE - if (hasOwn(target, reactiveFlag)) { - return target[reactiveFlag] + const proxyMap = isReadonly ? readonlyMap : reactiveMap + const existingProxy = proxyMap.get(target) + if (existingProxy) { + return existingProxy } // only a whitelist of value types can be observed. const targetType = getTargetType(target) if (targetType === TargetType.INVALID) { return target } - const observed = new Proxy( + const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ) - def(target, reactiveFlag, observed) - return observed + proxyMap.set(target, proxy) + return proxy } export function isReactive(value: unknown): boolean {