Skip to content

Commit 255dc72

Browse files
pikaxantfu
andauthored
feat: improve reactive checks (#502)
* feat: improve reactive checks * chore: add new array items as reactive * chore: add polyfil for IE * chore: removed symbols * chore: update readme * Update test/v3/reactivity/reactive.spec.ts * chore: add extra test * chore: removing array sub * chore: fix tests Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
1 parent 78592bf commit 255dc72

File tree

8 files changed

+89
-95
lines changed

8 files changed

+89
-95
lines changed

README.md

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -152,33 +152,6 @@ b.list[0].count.value === 0 // true
152152

153153
</details>
154154

155-
<details>
156-
<summary>
157-
✅ <b>Should</b> always use <code>ref</code> in a <code>reactive</code> when working with <code>Array</code>
158-
</summary>
159-
160-
```js
161-
const a = reactive({
162-
list: [
163-
reactive({
164-
count: ref(0),
165-
}),
166-
]
167-
})
168-
// unwrapped
169-
a.list[0].count === 0 // true
170-
171-
a.list.push(
172-
reactive({
173-
count: ref(1),
174-
})
175-
)
176-
// unwrapped
177-
a.list[1].count === 1 // true
178-
```
179-
180-
</details>
181-
182155
<details>
183156
<summary>
184157
⚠️ `set` workaround for adding new reactive properties

src/install.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { VueConstructor } from 'vue'
22
import { AnyObject } from './types/basic'
33
import { hasSymbol, hasOwn, isPlainObject, assert } from './utils'
4-
import { isRef } from './reactivity'
4+
import { isRef, markReactive } from './reactivity'
55
import {
66
setVueConstructor,
77
isVueRegistered,
@@ -72,6 +72,14 @@ export function install(Vue: VueConstructor) {
7272
}
7373
}
7474

75+
const observable = Vue.observable
76+
77+
Vue.observable = (obj: any) => {
78+
const o = observable(obj)
79+
markReactive(o)
80+
return o
81+
}
82+
7583
setVueConstructor(Vue)
7684
mixin(Vue)
7785
}

src/reactivity/reactive.ts

Lines changed: 23 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,21 @@
11
import { AnyObject } from '../types/basic'
22
import { getVueConstructor } from '../runtimeContext'
3-
import { isPlainObject, def, hasOwn, warn, isObject } from '../utils'
3+
import { isPlainObject, def, warn } from '../utils'
44
import { isComponentInstance, defineComponentInstance } from '../utils/helper'
5-
import {
6-
AccessControlIdentifierKey,
7-
ReactiveIdentifierKey,
8-
RawIdentifierKey,
9-
ReadonlyIdentifierKey,
10-
RefKey,
11-
} from '../utils/symbols'
5+
import { RefKey } from '../utils/symbols'
126
import { isRef, UnwrapRef } from './ref'
13-
14-
const AccessControlIdentifier = {}
15-
const ReactiveIdentifier = {}
16-
const RawIdentifier = {}
7+
import { rawSet, readonlySet, reactiveSet } from '../utils/sets'
178

189
export function isRaw(obj: any): boolean {
19-
return (
20-
hasOwn(obj, RawIdentifierKey) && obj[RawIdentifierKey] === RawIdentifier
21-
)
10+
return rawSet.has(obj)
2211
}
2312

2413
export function isReadonly(obj: any): boolean {
25-
return hasOwn(obj, ReadonlyIdentifierKey) && obj[ReadonlyIdentifierKey]
14+
return readonlySet.has(obj)
2615
}
2716

2817
export function isReactive(obj: any): boolean {
29-
return (
30-
isObject(obj) &&
31-
Object.isExtensible(obj) &&
32-
hasOwn(obj, ReactiveIdentifierKey) &&
33-
obj[ReactiveIdentifierKey] === ReactiveIdentifier
34-
)
18+
return reactiveSet.has(obj)
3519
}
3620

3721
/**
@@ -45,20 +29,9 @@ function setupAccessControl(target: AnyObject): void {
4529
Array.isArray(target) ||
4630
isRef(target) ||
4731
isComponentInstance(target)
48-
) {
49-
return
50-
}
51-
52-
if (
53-
hasOwn(target, AccessControlIdentifierKey) &&
54-
target[AccessControlIdentifierKey] === AccessControlIdentifier
55-
) {
32+
)
5633
return
57-
}
5834

59-
if (Object.isExtensible(target)) {
60-
def(target, AccessControlIdentifierKey, AccessControlIdentifier)
61-
}
6235
const keys = Object.keys(target)
6336
for (let i = 0; i < keys.length; i++) {
6437
defineAccessControl(target, keys[i])
@@ -203,29 +176,32 @@ export function shallowReactive<T extends object = any>(obj: T): T {
203176

204177
export function markReactive(target: any, shallow = false) {
205178
if (
206-
!isPlainObject(target) ||
179+
!(isPlainObject(target) || Array.isArray(target)) ||
180+
// !isPlainObject(target) ||
207181
isRaw(target) ||
208-
Array.isArray(target) ||
182+
// Array.isArray(target) ||
209183
isRef(target) ||
210184
isComponentInstance(target)
211185
) {
212186
return
213187
}
214188

215-
if (
216-
hasOwn(target, ReactiveIdentifierKey) &&
217-
target[ReactiveIdentifierKey] === ReactiveIdentifier
218-
) {
189+
if (isReactive(target) || !Object.isExtensible(target)) {
219190
return
220191
}
221192

222-
if (Object.isExtensible(target)) {
223-
def(target, ReactiveIdentifierKey, ReactiveIdentifier)
224-
}
193+
reactiveSet.add(target)
225194

226195
if (shallow) {
227196
return
228197
}
198+
199+
if (Array.isArray(target)) {
200+
// TODO way to track new array items
201+
target.forEach((x) => markReactive(x))
202+
return
203+
}
204+
229205
const keys = Object.keys(target)
230206
for (let i = 0; i < keys.length; i++) {
231207
markReactive(target[keys[i]])
@@ -264,9 +240,7 @@ export function shallowReadonly<T extends object>(obj: T): Readonly<T> {
264240
return obj // just typing
265241
}
266242

267-
const readonlyObj = {
268-
[ReadonlyIdentifierKey]: true,
269-
}
243+
const readonlyObj = {}
270244

271245
const source = reactive({})
272246
const ob = (source as any).__ob__
@@ -306,6 +280,8 @@ export function shallowReadonly<T extends object>(obj: T): Readonly<T> {
306280
})
307281
}
308282

283+
readonlySet.add(readonlyObj)
284+
309285
return readonlyObj as any
310286
}
311287

@@ -320,7 +296,7 @@ export function markRaw<T extends object>(obj: T): T {
320296
// set the vue observable flag at obj
321297
def(obj, '__ob__', (observe({}) as any).__ob__)
322298
// mark as Raw
323-
def(obj, RawIdentifierKey, RawIdentifier)
299+
rawSet.add(obj)
324300

325301
return obj
326302
}

src/reactivity/ref.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Data } from '../component'
2-
import { RefKey, ReadonlyIdentifierKey } from '../utils/symbols'
2+
import { RefKey } from '../utils/symbols'
33
import { proxy, isPlainObject, warn } from '../utils'
44
import { reactive, isReactive, shallowReactive } from './reactive'
5+
import { readonlySet } from '../utils/sets'
56

67
declare const _refBrand: unique symbol
78
export interface Ref<T = any> {
@@ -84,15 +85,13 @@ class RefImpl<T> implements Ref<T> {
8485

8586
export function createRef<T>(options: RefOption<T>, readonly = false) {
8687
const r = new RefImpl<T>(options)
87-
if (readonly) {
88-
//@ts-ignore
89-
r[ReadonlyIdentifierKey] = readonly
90-
}
91-
9288
// seal the ref, this could prevent ref from being observed
9389
// It's safe to seal the ref, since we really shouldn't extend it.
9490
// related issues: #79
95-
return Object.seal(r)
91+
const sealed = Object.seal(r)
92+
93+
readonlySet.add(sealed)
94+
return sealed
9695
}
9796

9897
export function ref<T extends object>(

src/utils/sets.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
if (!('WeakSet' in window)) {
2+
// simple polyfil for IE
3+
Object.defineProperty(window, 'WeakSet', {
4+
value: new (class {
5+
constructor(private _map = new WeakMap()) {}
6+
has(v: object): boolean {
7+
return this._map.has(v)
8+
}
9+
add(v: object) {
10+
return this._map.set(v, true)
11+
}
12+
remove(v: object) {
13+
return this._map.set(v, true)
14+
}
15+
})(),
16+
})
17+
}
18+
19+
export const reactiveSet = new WeakSet()
20+
export const rawSet = new WeakSet()
21+
export const readonlySet = new WeakSet()

src/utils/symbols.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,6 @@ export const WatcherPreFlushQueueKey = createSymbol(
1010
export const WatcherPostFlushQueueKey = createSymbol(
1111
'composition-api.postFlushQueue'
1212
)
13-
export const AccessControlIdentifierKey = createSymbol(
14-
'composition-api.accessControlIdentifier'
15-
)
16-
export const ReactiveIdentifierKey = createSymbol(
17-
'composition-api.reactiveIdentifier'
18-
)
19-
export const RawIdentifierKey = createSymbol('composition-api.rawIdentifierKey')
20-
export const ReadonlyIdentifierKey = createSymbol(
21-
'composition-api.readonlyIdentifierKey'
22-
)
2313

2414
// must be a string, symbol key is ignored in reactive
2515
export const RefKey = 'composition-api.refKey'

test/misc.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Vue from './vue'
2-
import { ref, nextTick } from '../src'
2+
import { ref, nextTick, isReactive } from '../src'
33

44
describe('nextTick', () => {
55
it('should works with callbacks', () => {
@@ -50,3 +50,30 @@ describe('nextTick', () => {
5050
expect(vm.$el.textContent).toBe('3')
5151
})
5252
})
53+
54+
describe('observable', () => {
55+
it('observable should be reactive', () => {
56+
const o: Record<string, any> = Vue.observable({
57+
a: 1,
58+
b: [{ a: 1 }],
59+
})
60+
61+
expect(isReactive(o)).toBe(true)
62+
63+
expect(isReactive(o.b)).toBe(true)
64+
expect(isReactive(o.b[0])).toBe(true)
65+
66+
// TODO new array items should be reactive
67+
// o.b.push({ a: 2 })
68+
// expect(isReactive(o.b[1])).toBe(true)
69+
})
70+
71+
it('nested deps should keep __ob__', () => {
72+
const o: any = Vue.observable({
73+
a: { b: 1 },
74+
})
75+
76+
expect(o.__ob__).not.toBeUndefined()
77+
expect(o.a.__ob__).not.toBeUndefined()
78+
})
79+
})

test/v3/reactivity/reactive.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ describe('reactivity/reactive', () => {
5757
}
5858
const observed = reactive(original)
5959
expect(isReactive(observed.nested)).toBe(true)
60-
// expect(isReactive(observed.array)).toBe(true); //not supported by vue2
61-
// expect(isReactive(observed.array[0])).toBe(true); //not supported by vue2
60+
expect(isReactive(observed.array)).toBe(true)
61+
expect(isReactive(observed.array[0])).toBe(true)
6262
})
6363

6464
test('observed value should proxy mutations to original (Object)', () => {

0 commit comments

Comments
 (0)