Skip to content

Commit f204daa

Browse files
committed
fix: circular objects and making all Vue.observable objects isReactive (#512)
1 parent 89fd11c commit f204daa

File tree

12 files changed

+138
-179
lines changed

12 files changed

+138
-179
lines changed

src/apis/state.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ export {
22
isReactive,
33
isRef,
44
markRaw,
5-
markReactive,
65
reactive,
76
ref,
87
customRef,

src/install.ts

Lines changed: 1 addition & 9 deletions
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, warn } from './utils'
4-
import { isRef, markReactive } from './reactivity'
4+
import { isRef } from './reactivity'
55
import { setVueConstructor, isVueRegistered } from './runtimeContext'
66
import { mixin } from './mixin'
77

@@ -72,14 +72,6 @@ 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-
8375
setVueConstructor(Vue)
8476
mixin(Vue)
8577
}

src/mixin.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ import {
55
SetupFunction,
66
Data,
77
} from './component'
8-
import { isRef, isReactive, markRaw, markReactive, toRefs } from './reactivity'
9-
import { isPlainObject, assert, proxy, warn, isFunction } from './utils'
8+
import { isRef, isReactive, toRefs } from './reactivity'
9+
import {
10+
isPlainObject,
11+
assert,
12+
proxy,
13+
warn,
14+
isFunction,
15+
isObject,
16+
def,
17+
} from './utils'
1018
import { ref } from './apis'
1119
import vmStateManager from './utils/vmStateManager'
1220
import {
@@ -15,6 +23,7 @@ import {
1523
resolveScopedSlots,
1624
asVmProperty,
1725
} from './utils/instance'
26+
import { PropsReactive } from './utils/symbols'
1827

1928
export function mixin(Vue: VueConstructor) {
2029
Vue.mixin({
@@ -73,14 +82,15 @@ export function mixin(Vue: VueConstructor) {
7382
const setup = vm.$options.setup!
7483
const ctx = createSetupContext(vm)
7584

76-
// mark props
77-
markReactive(props)
85+
// fake reactive for `toRefs(props)`
86+
def(props, PropsReactive, true)
7887

7988
// resolve scopedSlots and slots to functions
8089
resolveScopedSlots(vm, ctx.slots)
8190

8291
let binding: ReturnType<SetupFunction<Data, Data>> | undefined | null
8392
activateCurrentInstance(vm, () => {
93+
// make props to be fake reactive, this is for `toRefs(props)`
8494
binding = setup(props, ctx)
8595
})
8696

@@ -99,22 +109,19 @@ export function mixin(Vue: VueConstructor) {
99109
binding = toRefs(binding) as Data
100110
}
101111

102-
const bindingObj = binding
103112
vmStateManager.set(vm, 'rawBindings', binding)
113+
const bindingObj = binding
104114

105-
Object.keys(binding).forEach((name) => {
115+
Object.keys(bindingObj).forEach((name) => {
106116
let bindingValue: any = bindingObj[name]
107-
// only make primitive value reactive
117+
108118
if (!isRef(bindingValue)) {
109-
if (isReactive(bindingValue)) {
110-
bindingValue = ref(bindingValue)
111-
} else {
112-
// bind function to the vm, this will make `this` = vm
119+
if (!isReactive(bindingValue)) {
113120
if (isFunction(bindingValue)) {
114121
bindingValue = bindingValue.bind(vm)
122+
} else if (!isObject(bindingValue)) {
123+
bindingValue = ref(bindingValue)
115124
}
116-
// a non-reactive should not don't get reactivity
117-
bindingValue = ref(markRaw(bindingValue))
118125
}
119126
}
120127
asVmProperty(vm, name, bindingValue)

src/reactivity/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export {
55
shallowReactive,
66
toRaw,
77
isRaw,
8-
markReactive,
98
isReadonly,
109
shallowReadonly,
1110
} from './reactive'

src/reactivity/reactive.ts

Lines changed: 24 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ import { isPlainObject, def, warn } from '../utils'
44
import { isComponentInstance, defineComponentInstance } from '../utils/helper'
55
import { RefKey } from '../utils/symbols'
66
import { isRef, UnwrapRef } from './ref'
7-
import { rawSet, readonlySet, reactiveSet } from '../utils/sets'
7+
import { rawSet, accessModifiedSet, readonlySet } from '../utils/sets'
88

99
export function isRaw(obj: any): boolean {
10-
return rawSet.has(obj)
10+
return Boolean(obj?.__ob__ && obj.__ob__?.__raw__)
1111
}
1212

1313
export function isReadonly(obj: any): boolean {
1414
return readonlySet.has(obj)
1515
}
1616

1717
export function isReactive(obj: any): boolean {
18-
return reactiveSet.has(obj)
18+
return Boolean(obj?.__ob__ && !obj.__ob__?.__raw__)
1919
}
2020

2121
/**
@@ -28,10 +28,13 @@ function setupAccessControl(target: AnyObject): void {
2828
isRaw(target) ||
2929
Array.isArray(target) ||
3030
isRef(target) ||
31-
isComponentInstance(target)
31+
isComponentInstance(target) ||
32+
accessModifiedSet.has(target)
3233
)
3334
return
3435

36+
accessModifiedSet.set(target, true)
37+
3538
const keys = Object.keys(target)
3639
for (let i = 0; i < keys.length; i++) {
3740
defineAccessControl(target, keys[i])
@@ -43,6 +46,7 @@ function setupAccessControl(target: AnyObject): void {
4346
*/
4447
export function defineAccessControl(target: AnyObject, key: any, val?: any) {
4548
if (key === '__ob__') return
49+
if (isRaw(target[key])) return
4650

4751
let getter: (() => any) | undefined
4852
let setter: ((x: any) => void) | undefined
@@ -110,24 +114,18 @@ function observe<T>(obj: T): T {
110114
return observed
111115
}
112116

113-
export function shallowReactive<T extends object = any>(obj: T): T {
117+
export function shallowReactive<T extends object = any>(obj: T): T
118+
export function shallowReactive(obj: any): any {
114119
if (__DEV__ && !obj) {
115120
warn('"shallowReactive()" is called without provide an "object".')
116-
// @ts-ignore
117121
return
118122
}
119123

120-
if (
121-
!isPlainObject(obj) ||
122-
isReactive(obj) ||
123-
isRaw(obj) ||
124-
!Object.isExtensible(obj)
125-
) {
124+
if (!isPlainObject(obj) || isRaw(obj) || !Object.isExtensible(obj)) {
126125
return obj as any
127126
}
128127

129128
const observed = observe({})
130-
markReactive(observed, true)
131129
setupAccessControl(observed)
132130

133131
const ob = (observed as any).__ob__
@@ -151,7 +149,6 @@ export function shallowReactive<T extends object = any>(obj: T): T {
151149
}
152150
}
153151

154-
// setupAccessControl(val);
155152
Object.defineProperty(observed, key, {
156153
enumerable: true,
157154
configurable: true,
@@ -171,41 +168,7 @@ export function shallowReactive<T extends object = any>(obj: T): T {
171168
},
172169
})
173170
}
174-
return (observed as unknown) as T
175-
}
176-
177-
export function markReactive(target: any, shallow = false) {
178-
if (
179-
!(isPlainObject(target) || Array.isArray(target)) ||
180-
// !isPlainObject(target) ||
181-
isRaw(target) ||
182-
// Array.isArray(target) ||
183-
isRef(target) ||
184-
isComponentInstance(target)
185-
) {
186-
return
187-
}
188-
189-
if (isReactive(target) || !Object.isExtensible(target)) {
190-
return
191-
}
192-
193-
reactiveSet.add(target)
194-
195-
if (shallow) {
196-
return
197-
}
198-
199-
if (Array.isArray(target)) {
200-
// TODO way to track new array items
201-
target.forEach((x) => markReactive(x))
202-
return
203-
}
204-
205-
const keys = Object.keys(target)
206-
for (let i = 0; i < keys.length; i++) {
207-
markReactive(target[keys[i]])
208-
}
171+
return observed
209172
}
210173

211174
/**
@@ -218,26 +181,19 @@ export function reactive<T extends object>(obj: T): UnwrapRef<T> {
218181
return
219182
}
220183

221-
if (
222-
!isPlainObject(obj) ||
223-
isReactive(obj) ||
224-
isRaw(obj) ||
225-
!Object.isExtensible(obj)
226-
) {
184+
if (!isPlainObject(obj) || isRaw(obj) || !Object.isExtensible(obj)) {
227185
return obj as any
228186
}
229187

230188
const observed = observe(obj)
231-
// def(obj, ReactiveIdentifierKey, ReactiveIdentifier);
232-
markReactive(obj)
233189
setupAccessControl(observed)
234190
return observed as UnwrapRef<T>
235191
}
236192

237-
export function shallowReadonly<T extends object>(obj: T): Readonly<T> {
193+
export function shallowReadonly<T extends object>(obj: T): Readonly<T>
194+
export function shallowReadonly(obj: any): any {
238195
if (!isPlainObject(obj) || !Object.isExtensible(obj)) {
239-
//@ts-ignore
240-
return obj // just typing
196+
return obj
241197
}
242198

243199
const readonlyObj = {}
@@ -280,9 +236,9 @@ export function shallowReadonly<T extends object>(obj: T): Readonly<T> {
280236
})
281237
}
282238

283-
readonlySet.add(readonlyObj)
239+
readonlySet.set(readonlyObj, true)
284240

285-
return readonlyObj as any
241+
return readonlyObj
286242
}
287243

288244
/**
@@ -294,9 +250,12 @@ export function markRaw<T extends object>(obj: T): T {
294250
}
295251

296252
// set the vue observable flag at obj
297-
def(obj, '__ob__', (observe({}) as any).__ob__)
253+
const ob = (observe({}) as any).__ob__
254+
ob.__raw__ = true
255+
def(obj, '__ob__', ob)
256+
298257
// mark as Raw
299-
rawSet.add(obj)
258+
rawSet.set(obj, true)
300259

301260
return obj
302261
}
@@ -306,5 +265,5 @@ export function toRaw<T>(observed: T): T {
306265
return observed
307266
}
308267

309-
return (observed as any).__ob__.value || observed
268+
return (observed as any)?.__ob__?.value || observed
310269
}

src/reactivity/ref.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Data } from '../component'
2-
import { RefKey } from '../utils/symbols'
2+
import { RefKey, PropsReactive } from '../utils/symbols'
33
import { proxy, isPlainObject, warn } from '../utils'
44
import { reactive, isReactive, shallowReactive } from './reactive'
55
import { readonlySet } from '../utils/sets'
@@ -90,7 +90,7 @@ export function createRef<T>(options: RefOption<T>, readonly = false) {
9090
// related issues: #79
9191
const sealed = Object.seal(r)
9292

93-
readonlySet.add(sealed)
93+
readonlySet.set(sealed, true)
9494
return sealed
9595
}
9696

@@ -114,17 +114,19 @@ export function ref(raw?: unknown) {
114114
export function isRef<T>(value: any): value is Ref<T> {
115115
return value instanceof RefImpl
116116
}
117+
function isPropObject(obj: unknown) {
118+
return obj && typeof obj === 'object' && PropsReactive in obj
119+
}
117120

118121
export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
119122
return isRef(ref) ? (ref.value as any) : ref
120123
}
121124

122125
export function toRefs<T extends Data = Data>(obj: T): ToRefs<T> {
123-
if (!isPlainObject(obj)) return obj as any
124-
125-
if (__DEV__ && !isReactive(obj)) {
126+
if (__DEV__ && !isReactive(obj) && !isPropObject(obj)) {
126127
warn(`toRefs() expects a reactive object but received a plain one.`)
127128
}
129+
if (!isPlainObject(obj)) return obj as any
128130

129131
const ret: any = {}
130132
for (const key in obj) {

src/reactivity/set.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getVueConstructor } from '../runtimeContext'
22
import { isArray } from '../utils'
3-
import { defineAccessControl, markReactive } from './reactive'
3+
import { defineAccessControl } from './reactive'
44

55
function isUndef(v: any): boolean {
66
return v === undefined || v === null
@@ -59,7 +59,6 @@ export function set<T>(target: any, key: any, val: T): T {
5959
defineReactive(ob.value, key, val)
6060
// IMPORTANT: define access control before trigger watcher
6161
defineAccessControl(target, key, val)
62-
markReactive(ob.value[key])
6362

6463
ob.dep.notify()
6564
return val

0 commit comments

Comments
 (0)