Browse Source

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
Evan You 5 years ago
parent
commit
016ba116a8

+ 2 - 2
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) {

+ 10 - 5
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
     }

+ 9 - 9
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)
+    })
   }
 }
 

+ 12 - 14
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<Target, any>()
+export const readonlyMap = new WeakMap<Target, any>()
+
 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 {