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
This commit is contained in:
Evan You 2020-08-24 15:26:53 -04:00
parent 410e7abbbb
commit 016ba116a8
4 changed files with 33 additions and 30 deletions

View File

@ -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) {

View File

@ -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
}

View File

@ -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
!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.
function wrappedCallback(value: unknown, key: unknown) {
return callback.call(thisArg, wrap(value), wrap(key), observed)
}
return getProto(target).forEach.call(target, wrappedCallback)
})
}
}

View File

@ -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 {