Skip to content

Commit

Permalink
fix(reactivity): fix iOS 12 JSON.stringify error on reactive objects
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
yyx990803 committed Aug 24, 2020
1 parent 410e7ab commit 016ba11
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 30 deletions.
4 changes: 2 additions & 2 deletions packages/reactivity/__tests__/readonly.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
15 changes: 10 additions & 5 deletions packages/reactivity/src/baseHandlers.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
18 changes: 9 additions & 9 deletions packages/reactivity/src/collectionHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}

Expand Down
26 changes: 12 additions & 14 deletions packages/reactivity/src/reactive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isObject, toRawType, def, hasOwn } from '@vue/shared'
import { isObject, toRawType, def } from '@vue/shared'
import {
mutableHandlers,
readonlyHandlers,
Expand All @@ -16,20 +16,19 @@ 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 {
[ReactiveFlags.SKIP]?: boolean
[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,
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 016ba11

Please sign in to comment.