Skip to content

Commit 5dcc645

Browse files
committed
fix(reactivity): track reactive keys in raw collection types
Also warn against presence of both raw and reactive versions of the same object in a collection as keys. fix #919
1 parent 7402951 commit 5dcc645

File tree

3 files changed

+126
-6
lines changed

3 files changed

+126
-6
lines changed

packages/reactivity/__tests__/collections/Map.spec.ts

+48
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { reactive, effect, toRaw, isReactive } from '../../src'
2+
import { mockWarn } from '@vue/shared'
23

34
describe('reactivity/collections', () => {
45
describe('Map', () => {
6+
mockWarn()
7+
58
test('instanceof', () => {
69
const original = new Map()
710
const observed = reactive(original)
@@ -363,6 +366,51 @@ describe('reactivity/collections', () => {
363366
expect(map.get(key)).toBeUndefined()
364367
})
365368

369+
it('should track set of reactive keys in raw map', () => {
370+
const raw = new Map()
371+
const key = reactive({})
372+
raw.set(key, 1)
373+
const map = reactive(raw)
374+
375+
let dummy
376+
effect(() => {
377+
dummy = map.get(key)
378+
})
379+
expect(dummy).toBe(1)
380+
381+
map.set(key, 2)
382+
expect(dummy).toBe(2)
383+
})
384+
385+
it('should track deletion of reactive keys in raw map', () => {
386+
const raw = new Map()
387+
const key = reactive({})
388+
raw.set(key, 1)
389+
const map = reactive(raw)
390+
391+
let dummy
392+
effect(() => {
393+
dummy = map.has(key)
394+
})
395+
expect(dummy).toBe(true)
396+
397+
map.delete(key)
398+
expect(dummy).toBe(false)
399+
})
400+
401+
it('should warn when both raw and reactive versions of the same object is used as key', () => {
402+
const raw = new Map()
403+
const rawKey = {}
404+
const key = reactive(rawKey)
405+
raw.set(rawKey, 1)
406+
raw.set(key, 1)
407+
const map = reactive(raw)
408+
map.set(key, 2)
409+
expect(
410+
`Reactive Map contains both the raw and reactive`
411+
).toHaveBeenWarned()
412+
})
413+
366414
// #877
367415
it('should not trigger key iteration when setting existing keys', () => {
368416
const map = reactive(new Map())

packages/reactivity/__tests__/collections/Set.spec.ts

+32
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { reactive, effect, isReactive, toRaw } from '../../src'
2+
import { mockWarn } from '@vue/shared'
23

34
describe('reactivity/collections', () => {
45
describe('Set', () => {
6+
mockWarn()
7+
58
it('instanceof', () => {
69
const original = new Set()
710
const observed = reactive(original)
@@ -380,5 +383,34 @@ describe('reactivity/collections', () => {
380383
expect(set.delete(entry)).toBe(true)
381384
expect(set.has(entry)).toBe(false)
382385
})
386+
387+
it('should track deletion of reactive entries in raw set', () => {
388+
const raw = new Set()
389+
const entry = reactive({})
390+
raw.add(entry)
391+
const set = reactive(raw)
392+
393+
let dummy
394+
effect(() => {
395+
dummy = set.has(entry)
396+
})
397+
expect(dummy).toBe(true)
398+
399+
set.delete(entry)
400+
expect(dummy).toBe(false)
401+
})
402+
403+
it('should warn when set contains both raw and reactive versions of the same object', () => {
404+
const raw = new Set()
405+
const rawKey = {}
406+
const key = reactive(rawKey)
407+
raw.add(rawKey)
408+
raw.add(key)
409+
const set = reactive(raw)
410+
set.delete(key)
411+
expect(
412+
`Reactive Set contains both the raw and reactive`
413+
).toHaveBeenWarned()
414+
})
383415
})
384416
})

packages/reactivity/src/collectionHandlers.ts

+46-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { toRaw, reactive, readonly } from './reactive'
22
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
33
import { TrackOpTypes, TriggerOpTypes } from './operations'
44
import { LOCKED } from './lock'
5-
import { isObject, capitalize, hasOwn, hasChanged } from '@vue/shared'
5+
import {
6+
isObject,
7+
capitalize,
8+
hasOwn,
9+
hasChanged,
10+
toRawType
11+
} from '@vue/shared'
612

713
export type CollectionTypes = IterableCollections | WeakCollections
814

@@ -27,6 +33,9 @@ function get(
2733
) {
2834
target = toRaw(target)
2935
const rawKey = toRaw(key)
36+
if (key !== rawKey) {
37+
track(target, TrackOpTypes.GET, key)
38+
}
3039
track(target, TrackOpTypes.GET, rawKey)
3140
const { has, get } = getProto(target)
3241
if (has.call(target, key)) {
@@ -39,6 +48,9 @@ function get(
3948
function has(this: CollectionTypes, key: unknown): boolean {
4049
const target = toRaw(this)
4150
const rawKey = toRaw(key)
51+
if (key !== rawKey) {
52+
track(target, TrackOpTypes.HAS, key)
53+
}
4254
track(target, TrackOpTypes.HAS, rawKey)
4355
const has = getProto(target).has
4456
return has.call(target, key) || has.call(target, rawKey)
@@ -64,12 +76,19 @@ function add(this: SetTypes, value: unknown) {
6476

6577
function set(this: MapTypes, key: unknown, value: unknown) {
6678
value = toRaw(value)
67-
key = toRaw(key)
6879
const target = toRaw(this)
69-
const proto = getProto(target)
70-
const hadKey = proto.has.call(target, key)
71-
const oldValue = proto.get.call(target, key)
72-
const result = proto.set.call(target, key, value)
80+
const { has, get, set } = getProto(target)
81+
82+
let hadKey = has.call(target, key)
83+
if (!hadKey) {
84+
key = toRaw(key)
85+
hadKey = has.call(target, key)
86+
} else if (__DEV__) {
87+
checkIdentitiyKeys(target, has, key)
88+
}
89+
90+
const oldValue = get.call(target, key)
91+
const result = set.call(target, key, value)
7392
if (!hadKey) {
7493
trigger(target, TriggerOpTypes.ADD, key, value)
7594
} else if (hasChanged(value, oldValue)) {
@@ -85,7 +104,10 @@ function deleteEntry(this: CollectionTypes, key: unknown) {
85104
if (!hadKey) {
86105
key = toRaw(key)
87106
hadKey = has.call(target, key)
107+
} else if (__DEV__) {
108+
checkIdentitiyKeys(target, has, key)
88109
}
110+
89111
const oldValue = get ? get.call(target, key) : undefined
90112
// forward the operation before queueing reactions
91113
const result = del.call(target, key)
@@ -251,3 +273,21 @@ export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
251273
export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
252274
get: createInstrumentationGetter(readonlyInstrumentations)
253275
}
276+
277+
function checkIdentitiyKeys(
278+
target: CollectionTypes,
279+
has: (key: unknown) => boolean,
280+
key: unknown
281+
) {
282+
const rawKey = toRaw(key)
283+
if (rawKey !== key && has.call(target, rawKey)) {
284+
const type = toRawType(target)
285+
console.warn(
286+
`Reactive ${type} contains both the raw and reactive ` +
287+
`versions of the same object${type === `Map` ? `as keys` : ``}, ` +
288+
`which can lead to inconsistencies. ` +
289+
`Avoid differentiating between the raw and reactive versions ` +
290+
`of an object and only use the reactive version if possible.`
291+
)
292+
}
293+
}

0 commit comments

Comments
 (0)