Skip to content

Commit

Permalink
fix: circular objects and making all Vue.observable objects isReactive (
Browse files Browse the repository at this point in the history
  • Loading branch information
pikax committed Sep 15, 2020
1 parent 89fd11c commit f204daa
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 179 deletions.
1 change: 0 additions & 1 deletion src/apis/state.ts
Expand Up @@ -2,7 +2,6 @@ export {
isReactive,
isRef,
markRaw,
markReactive,
reactive,
ref,
customRef,
Expand Down
10 changes: 1 addition & 9 deletions src/install.ts
@@ -1,7 +1,7 @@
import type { VueConstructor } from 'vue'
import { AnyObject } from './types/basic'
import { hasSymbol, hasOwn, isPlainObject, assert, warn } from './utils'
import { isRef, markReactive } from './reactivity'
import { isRef } from './reactivity'
import { setVueConstructor, isVueRegistered } from './runtimeContext'
import { mixin } from './mixin'

Expand Down Expand Up @@ -72,14 +72,6 @@ export function install(Vue: VueConstructor) {
}
}

const observable = Vue.observable

Vue.observable = (obj: any) => {
const o = observable(obj)
markReactive(o)
return o
}

setVueConstructor(Vue)
mixin(Vue)
}
Expand Down
33 changes: 20 additions & 13 deletions src/mixin.ts
Expand Up @@ -5,8 +5,16 @@ import {
SetupFunction,
Data,
} from './component'
import { isRef, isReactive, markRaw, markReactive, toRefs } from './reactivity'
import { isPlainObject, assert, proxy, warn, isFunction } from './utils'
import { isRef, isReactive, toRefs } from './reactivity'
import {
isPlainObject,
assert,
proxy,
warn,
isFunction,
isObject,
def,
} from './utils'
import { ref } from './apis'
import vmStateManager from './utils/vmStateManager'
import {
Expand All @@ -15,6 +23,7 @@ import {
resolveScopedSlots,
asVmProperty,
} from './utils/instance'
import { PropsReactive } from './utils/symbols'

export function mixin(Vue: VueConstructor) {
Vue.mixin({
Expand Down Expand Up @@ -73,14 +82,15 @@ export function mixin(Vue: VueConstructor) {
const setup = vm.$options.setup!
const ctx = createSetupContext(vm)

// mark props
markReactive(props)
// fake reactive for `toRefs(props)`
def(props, PropsReactive, true)

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

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

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

const bindingObj = binding
vmStateManager.set(vm, 'rawBindings', binding)
const bindingObj = binding

Object.keys(binding).forEach((name) => {
Object.keys(bindingObj).forEach((name) => {
let bindingValue: any = bindingObj[name]
// only make primitive value reactive

if (!isRef(bindingValue)) {
if (isReactive(bindingValue)) {
bindingValue = ref(bindingValue)
} else {
// bind function to the vm, this will make `this` = vm
if (!isReactive(bindingValue)) {
if (isFunction(bindingValue)) {
bindingValue = bindingValue.bind(vm)
} else if (!isObject(bindingValue)) {
bindingValue = ref(bindingValue)
}
// a non-reactive should not don't get reactivity
bindingValue = ref(markRaw(bindingValue))
}
}
asVmProperty(vm, name, bindingValue)
Expand Down
1 change: 0 additions & 1 deletion src/reactivity/index.ts
Expand Up @@ -5,7 +5,6 @@ export {
shallowReactive,
toRaw,
isRaw,
markReactive,
isReadonly,
shallowReadonly,
} from './reactive'
Expand Down
89 changes: 24 additions & 65 deletions src/reactivity/reactive.ts
Expand Up @@ -4,18 +4,18 @@ import { isPlainObject, def, warn } from '../utils'
import { isComponentInstance, defineComponentInstance } from '../utils/helper'
import { RefKey } from '../utils/symbols'
import { isRef, UnwrapRef } from './ref'
import { rawSet, readonlySet, reactiveSet } from '../utils/sets'
import { rawSet, accessModifiedSet, readonlySet } from '../utils/sets'

export function isRaw(obj: any): boolean {
return rawSet.has(obj)
return Boolean(obj?.__ob__ && obj.__ob__?.__raw__)
}

export function isReadonly(obj: any): boolean {
return readonlySet.has(obj)
}

export function isReactive(obj: any): boolean {
return reactiveSet.has(obj)
return Boolean(obj?.__ob__ && !obj.__ob__?.__raw__)
}

/**
Expand All @@ -28,10 +28,13 @@ function setupAccessControl(target: AnyObject): void {
isRaw(target) ||
Array.isArray(target) ||
isRef(target) ||
isComponentInstance(target)
isComponentInstance(target) ||
accessModifiedSet.has(target)
)
return

accessModifiedSet.set(target, true)

const keys = Object.keys(target)
for (let i = 0; i < keys.length; i++) {
defineAccessControl(target, keys[i])
Expand All @@ -43,6 +46,7 @@ function setupAccessControl(target: AnyObject): void {
*/
export function defineAccessControl(target: AnyObject, key: any, val?: any) {
if (key === '__ob__') return
if (isRaw(target[key])) return

let getter: (() => any) | undefined
let setter: ((x: any) => void) | undefined
Expand Down Expand Up @@ -110,24 +114,18 @@ function observe<T>(obj: T): T {
return observed
}

export function shallowReactive<T extends object = any>(obj: T): T {
export function shallowReactive<T extends object = any>(obj: T): T
export function shallowReactive(obj: any): any {
if (__DEV__ && !obj) {
warn('"shallowReactive()" is called without provide an "object".')
// @ts-ignore
return
}

if (
!isPlainObject(obj) ||
isReactive(obj) ||
isRaw(obj) ||
!Object.isExtensible(obj)
) {
if (!isPlainObject(obj) || isRaw(obj) || !Object.isExtensible(obj)) {
return obj as any
}

const observed = observe({})
markReactive(observed, true)
setupAccessControl(observed)

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

// setupAccessControl(val);
Object.defineProperty(observed, key, {
enumerable: true,
configurable: true,
Expand All @@ -171,41 +168,7 @@ export function shallowReactive<T extends object = any>(obj: T): T {
},
})
}
return (observed as unknown) as T
}

export function markReactive(target: any, shallow = false) {
if (
!(isPlainObject(target) || Array.isArray(target)) ||
// !isPlainObject(target) ||
isRaw(target) ||
// Array.isArray(target) ||
isRef(target) ||
isComponentInstance(target)
) {
return
}

if (isReactive(target) || !Object.isExtensible(target)) {
return
}

reactiveSet.add(target)

if (shallow) {
return
}

if (Array.isArray(target)) {
// TODO way to track new array items
target.forEach((x) => markReactive(x))
return
}

const keys = Object.keys(target)
for (let i = 0; i < keys.length; i++) {
markReactive(target[keys[i]])
}
return observed
}

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

if (
!isPlainObject(obj) ||
isReactive(obj) ||
isRaw(obj) ||
!Object.isExtensible(obj)
) {
if (!isPlainObject(obj) || isRaw(obj) || !Object.isExtensible(obj)) {
return obj as any
}

const observed = observe(obj)
// def(obj, ReactiveIdentifierKey, ReactiveIdentifier);
markReactive(obj)
setupAccessControl(observed)
return observed as UnwrapRef<T>
}

export function shallowReadonly<T extends object>(obj: T): Readonly<T> {
export function shallowReadonly<T extends object>(obj: T): Readonly<T>
export function shallowReadonly(obj: any): any {
if (!isPlainObject(obj) || !Object.isExtensible(obj)) {
//@ts-ignore
return obj // just typing
return obj
}

const readonlyObj = {}
Expand Down Expand Up @@ -280,9 +236,9 @@ export function shallowReadonly<T extends object>(obj: T): Readonly<T> {
})
}

readonlySet.add(readonlyObj)
readonlySet.set(readonlyObj, true)

return readonlyObj as any
return readonlyObj
}

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

// set the vue observable flag at obj
def(obj, '__ob__', (observe({}) as any).__ob__)
const ob = (observe({}) as any).__ob__
ob.__raw__ = true
def(obj, '__ob__', ob)

// mark as Raw
rawSet.add(obj)
rawSet.set(obj, true)

return obj
}
Expand All @@ -306,5 +265,5 @@ export function toRaw<T>(observed: T): T {
return observed
}

return (observed as any).__ob__.value || observed
return (observed as any)?.__ob__?.value || observed
}
12 changes: 7 additions & 5 deletions src/reactivity/ref.ts
@@ -1,5 +1,5 @@
import { Data } from '../component'
import { RefKey } from '../utils/symbols'
import { RefKey, PropsReactive } from '../utils/symbols'
import { proxy, isPlainObject, warn } from '../utils'
import { reactive, isReactive, shallowReactive } from './reactive'
import { readonlySet } from '../utils/sets'
Expand Down Expand Up @@ -90,7 +90,7 @@ export function createRef<T>(options: RefOption<T>, readonly = false) {
// related issues: #79
const sealed = Object.seal(r)

readonlySet.add(sealed)
readonlySet.set(sealed, true)
return sealed
}

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

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

export function toRefs<T extends Data = Data>(obj: T): ToRefs<T> {
if (!isPlainObject(obj)) return obj as any

if (__DEV__ && !isReactive(obj)) {
if (__DEV__ && !isReactive(obj) && !isPropObject(obj)) {
warn(`toRefs() expects a reactive object but received a plain one.`)
}
if (!isPlainObject(obj)) return obj as any

const ret: any = {}
for (const key in obj) {
Expand Down
3 changes: 1 addition & 2 deletions src/reactivity/set.ts
@@ -1,6 +1,6 @@
import { getVueConstructor } from '../runtimeContext'
import { isArray } from '../utils'
import { defineAccessControl, markReactive } from './reactive'
import { defineAccessControl } from './reactive'

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

ob.dep.notify()
return val
Expand Down

0 comments on commit f204daa

Please sign in to comment.