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', () => { test('should retrieve readonly values on iteration', () => {
const key1 = {} const key1 = {}
const key2 = {} const key2 = {}
const original = new Collection([[key1, {}], [key2, {}]]) const original = new Map([[key1, {}], [key2, {}]])
const wrapped: any = readonly(original) const wrapped: any = readonly(original)
expect(wrapped.size).toBe(2) expect(wrapped.size).toBe(2)
for (const [key, value] of wrapped) { for (const [key, value] of wrapped) {
@ -246,7 +246,7 @@ describe('reactivity/readonly', () => {
test('should retrieve reactive + readonly values on iteration', () => { test('should retrieve reactive + readonly values on iteration', () => {
const key1 = {} const key1 = {}
const key2 = {} const key2 = {}
const original = reactive(new Collection([[key1, {}], [key2, {}]])) const original = reactive(new Map([[key1, {}], [key2, {}]]))
const wrapped: any = readonly(original) const wrapped: any = readonly(original)
expect(wrapped.size).toBe(2) expect(wrapped.size).toBe(2)
for (const [key, value] of wrapped) { 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 { TrackOpTypes, TriggerOpTypes } from './operations'
import { track, trigger, ITERATE_KEY } from './effect' import { track, trigger, ITERATE_KEY } from './effect'
import { import {
@ -48,10 +56,7 @@ function createGetter(isReadonly = false, shallow = false) {
return isReadonly return isReadonly
} else if ( } else if (
key === ReactiveFlags.RAW && key === ReactiveFlags.RAW &&
receiver === receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
(isReadonly
? target[ReactiveFlags.READONLY]
: target[ReactiveFlags.REACTIVE])
) { ) {
return target return target
} }

View File

@ -145,17 +145,17 @@ function createForEach(isReadonly: boolean, isShallow: boolean) {
callback: Function, callback: Function,
thisArg?: unknown thisArg?: unknown
) { ) {
const observed = this const observed = this as any
const target = toRaw(observed) const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
!isReadonly && track(target, TrackOpTypes.ITERATE, ITERATE_KEY) !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
// important: create sure the callback is return target.forEach((value: unknown, key: unknown) => {
// 1. invoked with the reactive map as `this` and 3rd arg // important: make sure the callback is
// 2. the value received should be a corresponding reactive/readonly. // 1. invoked with the reactive map as `this` and 3rd arg
function wrappedCallback(value: unknown, key: unknown) { // 2. the value received should be a corresponding reactive/readonly.
return callback.call(thisArg, wrap(value), wrap(key), observed) 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 { import {
mutableHandlers, mutableHandlers,
readonlyHandlers, readonlyHandlers,
@ -16,9 +16,7 @@ export const enum ReactiveFlags {
SKIP = '__v_skip', SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive', IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly', IS_READONLY = '__v_isReadonly',
RAW = '__v_raw', RAW = '__v_raw'
REACTIVE = '__v_reactive',
READONLY = '__v_readonly'
} }
export interface Target { export interface Target {
@ -26,10 +24,11 @@ export interface Target {
[ReactiveFlags.IS_REACTIVE]?: boolean [ReactiveFlags.IS_REACTIVE]?: boolean
[ReactiveFlags.IS_READONLY]?: boolean [ReactiveFlags.IS_READONLY]?: boolean
[ReactiveFlags.RAW]?: any [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 { const enum TargetType {
INVALID = 0, INVALID = 0,
COMMON = 1, COMMON = 1,
@ -155,23 +154,22 @@ function createReactiveObject(
return target return target
} }
// target already has corresponding Proxy // target already has corresponding Proxy
const reactiveFlag = isReadonly const proxyMap = isReadonly ? readonlyMap : reactiveMap
? ReactiveFlags.READONLY const existingProxy = proxyMap.get(target)
: ReactiveFlags.REACTIVE if (existingProxy) {
if (hasOwn(target, reactiveFlag)) { return existingProxy
return target[reactiveFlag]
} }
// only a whitelist of value types can be observed. // only a whitelist of value types can be observed.
const targetType = getTargetType(target) const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) { if (targetType === TargetType.INVALID) {
return target return target
} }
const observed = new Proxy( const proxy = new Proxy(
target, target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
) )
def(target, reactiveFlag, observed) proxyMap.set(target, proxy)
return observed return proxy
} }
export function isReactive(value: unknown): boolean { export function isReactive(value: unknown): boolean {