From 09b4202a22ae03072a8a8405511e37f65b626568 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 15 Apr 2020 16:45:20 -0400 Subject: [PATCH] refactor(reactivity): adjust APIs BREAKING CHANGE: Reactivity APIs adjustments: - `readonly` is now non-tracking if called on plain objects. `lock` and `unlock` have been removed. A `readonly` proxy can no longer be directly mutated. However, it can still wrap an already reactive object and track changes to the source reactive object. - `isReactive` now only returns true for proxies created by `reactive`, or a `readonly` proxy that wraps a `reactive` proxy. - A new utility `isProxy` is introduced, which returns true for both reactive or readonly proxies. - `markNonReactive` has been renamed to `markRaw`. --- packages/reactivity/__tests__/effect.spec.ts | 6 +- .../reactivity/__tests__/reactive.spec.ts | 6 +- .../reactivity/__tests__/readonly.spec.ts | 176 +++++++++--------- packages/reactivity/src/index.ts | 9 +- packages/reactivity/src/reactive.ts | 15 +- packages/reactivity/src/ref.ts | 4 +- packages/runtime-core/src/componentProps.ts | 4 +- packages/runtime-core/src/componentSlots.ts | 4 +- packages/runtime-core/src/index.ts | 14 +- packages/runtime-core/src/vnode.ts | 18 +- packages/runtime-test/src/nodeOps.ts | 8 +- 11 files changed, 139 insertions(+), 125 deletions(-) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 24ade5b7..f8a016b4 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -6,7 +6,7 @@ import { TrackOpTypes, TriggerOpTypes, DebuggerEvent, - markNonReactive, + markRaw, ref } from '../src/index' import { ITERATE_KEY } from '../src/effect' @@ -732,9 +732,9 @@ describe('reactivity/effect', () => { expect(dummy).toBe(3) }) - it('markNonReactive', () => { + it('markRaw', () => { const obj = reactive({ - foo: markNonReactive({ + foo: markRaw({ prop: 0 }) }) diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts index 9f716734..eb730ff3 100644 --- a/packages/reactivity/__tests__/reactive.spec.ts +++ b/packages/reactivity/__tests__/reactive.spec.ts @@ -3,7 +3,7 @@ import { reactive, isReactive, toRaw, - markNonReactive, + markRaw, shallowReactive } from '../src/reactive' import { mockWarn } from '@vue/shared' @@ -146,10 +146,10 @@ describe('reactivity/reactive', () => { expect(reactive(d)).toBe(d) }) - test('markNonReactive', () => { + test('markRaw', () => { const obj = reactive({ foo: { a: 1 }, - bar: markNonReactive({ b: 2 }) + bar: markRaw({ b: 2 }) }) expect(isReactive(obj.foo)).toBe(true) expect(isReactive(obj.bar)).toBe(false) diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts index 0f5b23be..12fb6d08 100644 --- a/packages/reactivity/__tests__/readonly.spec.ts +++ b/packages/reactivity/__tests__/readonly.spec.ts @@ -4,10 +4,11 @@ import { toRaw, isReactive, isReadonly, - markNonReactive, + markRaw, effect, ref, - shallowReadonly + shallowReadonly, + isProxy } from '../src' import { mockWarn } from '@vue/shared' @@ -22,22 +23,23 @@ describe('reactivity/readonly', () => { describe('Object', () => { it('should make nested values readonly', () => { const original = { foo: 1, bar: { baz: 2 } } - const observed = readonly(original) - expect(observed).not.toBe(original) - expect(isReactive(observed)).toBe(true) - expect(isReadonly(observed)).toBe(true) + const wrapped = readonly(original) + expect(wrapped).not.toBe(original) + expect(isProxy(wrapped)).toBe(true) + expect(isReactive(wrapped)).toBe(false) + expect(isReadonly(wrapped)).toBe(true) expect(isReactive(original)).toBe(false) expect(isReadonly(original)).toBe(false) - expect(isReactive(observed.bar)).toBe(true) - expect(isReadonly(observed.bar)).toBe(true) + expect(isReactive(wrapped.bar)).toBe(false) + expect(isReadonly(wrapped.bar)).toBe(true) expect(isReactive(original.bar)).toBe(false) expect(isReadonly(original.bar)).toBe(false) // get - expect(observed.foo).toBe(1) + expect(wrapped.foo).toBe(1) // has - expect('foo' in observed).toBe(true) + expect('foo' in wrapped).toBe(true) // ownKeys - expect(Object.keys(observed)).toEqual(['foo', 'bar']) + expect(Object.keys(wrapped)).toEqual(['foo', 'bar']) }) it('should not allow mutation', () => { @@ -49,54 +51,54 @@ describe('reactivity/readonly', () => { }, [qux]: 3 } - const observed: Writable = readonly(original) + const wrapped: Writable = readonly(original) - observed.foo = 2 - expect(observed.foo).toBe(1) + wrapped.foo = 2 + expect(wrapped.foo).toBe(1) expect( `Set operation on key "foo" failed: target is readonly.` ).toHaveBeenWarnedLast() - observed.bar.baz = 3 - expect(observed.bar.baz).toBe(2) + wrapped.bar.baz = 3 + expect(wrapped.bar.baz).toBe(2) expect( `Set operation on key "baz" failed: target is readonly.` ).toHaveBeenWarnedLast() - observed[qux] = 4 - expect(observed[qux]).toBe(3) + wrapped[qux] = 4 + expect(wrapped[qux]).toBe(3) expect( `Set operation on key "Symbol(qux)" failed: target is readonly.` ).toHaveBeenWarnedLast() - delete observed.foo - expect(observed.foo).toBe(1) + delete wrapped.foo + expect(wrapped.foo).toBe(1) expect( `Delete operation on key "foo" failed: target is readonly.` ).toHaveBeenWarnedLast() - delete observed.bar.baz - expect(observed.bar.baz).toBe(2) + delete wrapped.bar.baz + expect(wrapped.bar.baz).toBe(2) expect( `Delete operation on key "baz" failed: target is readonly.` ).toHaveBeenWarnedLast() - delete observed[qux] - expect(observed[qux]).toBe(3) + delete wrapped[qux] + expect(wrapped[qux]).toBe(3) expect( `Delete operation on key "Symbol(qux)" failed: target is readonly.` ).toHaveBeenWarnedLast() }) it('should not trigger effects', () => { - const observed: any = readonly({ a: 1 }) + const wrapped: any = readonly({ a: 1 }) let dummy effect(() => { - dummy = observed.a + dummy = wrapped.a }) expect(dummy).toBe(1) - observed.a = 2 - expect(observed.a).toBe(1) + wrapped.a = 2 + expect(wrapped.a).toBe(1) expect(dummy).toBe(1) expect(`target is readonly`).toHaveBeenWarned() }) @@ -105,65 +107,66 @@ describe('reactivity/readonly', () => { describe('Array', () => { it('should make nested values readonly', () => { const original = [{ foo: 1 }] - const observed = readonly(original) - expect(observed).not.toBe(original) - expect(isReactive(observed)).toBe(true) - expect(isReadonly(observed)).toBe(true) + const wrapped = readonly(original) + expect(wrapped).not.toBe(original) + expect(isProxy(wrapped)).toBe(true) + expect(isReactive(wrapped)).toBe(false) + expect(isReadonly(wrapped)).toBe(true) expect(isReactive(original)).toBe(false) expect(isReadonly(original)).toBe(false) - expect(isReactive(observed[0])).toBe(true) - expect(isReadonly(observed[0])).toBe(true) + expect(isReactive(wrapped[0])).toBe(false) + expect(isReadonly(wrapped[0])).toBe(true) expect(isReactive(original[0])).toBe(false) expect(isReadonly(original[0])).toBe(false) // get - expect(observed[0].foo).toBe(1) + expect(wrapped[0].foo).toBe(1) // has - expect(0 in observed).toBe(true) + expect(0 in wrapped).toBe(true) // ownKeys - expect(Object.keys(observed)).toEqual(['0']) + expect(Object.keys(wrapped)).toEqual(['0']) }) it('should not allow mutation', () => { - const observed: any = readonly([{ foo: 1 }]) - observed[0] = 1 - expect(observed[0]).not.toBe(1) + const wrapped: any = readonly([{ foo: 1 }]) + wrapped[0] = 1 + expect(wrapped[0]).not.toBe(1) expect( `Set operation on key "0" failed: target is readonly.` ).toHaveBeenWarned() - observed[0].foo = 2 - expect(observed[0].foo).toBe(1) + wrapped[0].foo = 2 + expect(wrapped[0].foo).toBe(1) expect( `Set operation on key "foo" failed: target is readonly.` ).toHaveBeenWarned() // should block length mutation - observed.length = 0 - expect(observed.length).toBe(1) - expect(observed[0].foo).toBe(1) + wrapped.length = 0 + expect(wrapped.length).toBe(1) + expect(wrapped[0].foo).toBe(1) expect( `Set operation on key "length" failed: target is readonly.` ).toHaveBeenWarned() // mutation methods invoke set/length internally and thus are blocked as well - observed.push(2) - expect(observed.length).toBe(1) + wrapped.push(2) + expect(wrapped.length).toBe(1) // push triggers two warnings on [1] and .length expect(`target is readonly.`).toHaveBeenWarnedTimes(5) }) it('should not trigger effects', () => { - const observed: any = readonly([{ a: 1 }]) + const wrapped: any = readonly([{ a: 1 }]) let dummy effect(() => { - dummy = observed[0].a + dummy = wrapped[0].a }) expect(dummy).toBe(1) - observed[0].a = 2 - expect(observed[0].a).toBe(1) + wrapped[0].a = 2 + expect(wrapped[0].a).toBe(1) expect(dummy).toBe(1) expect(`target is readonly`).toHaveBeenWarnedTimes(1) - observed[0] = { a: 2 } - expect(observed[0].a).toBe(1) + wrapped[0] = { a: 2 } + expect(wrapped[0].a).toBe(1) expect(dummy).toBe(1) expect(`target is readonly`).toHaveBeenWarnedTimes(2) }) @@ -176,14 +179,15 @@ describe('reactivity/readonly', () => { const key1 = {} const key2 = {} const original = new Collection([[key1, {}], [key2, {}]]) - const observed = readonly(original) - expect(observed).not.toBe(original) - expect(isReactive(observed)).toBe(true) - expect(isReadonly(observed)).toBe(true) + const wrapped = readonly(original) + expect(wrapped).not.toBe(original) + expect(isProxy(wrapped)).toBe(true) + expect(isReactive(wrapped)).toBe(false) + expect(isReadonly(wrapped)).toBe(true) expect(isReactive(original)).toBe(false) expect(isReadonly(original)).toBe(false) - expect(isReactive(observed.get(key1))).toBe(true) - expect(isReadonly(observed.get(key1))).toBe(true) + expect(isReactive(wrapped.get(key1))).toBe(false) + expect(isReadonly(wrapped.get(key1))).toBe(true) expect(isReactive(original.get(key1))).toBe(false) expect(isReadonly(original.get(key1))).toBe(false) }) @@ -209,15 +213,15 @@ describe('reactivity/readonly', () => { const key1 = {} const key2 = {} const original = new Collection([[key1, {}], [key2, {}]]) - const observed: any = readonly(original) - for (const [key, value] of observed) { + const wrapped: any = readonly(original) + for (const [key, value] of wrapped) { expect(isReadonly(key)).toBe(true) expect(isReadonly(value)).toBe(true) } - observed.forEach((value: any) => { + wrapped.forEach((value: any) => { expect(isReadonly(value)).toBe(true) }) - for (const value of observed.values()) { + for (const value of wrapped.values()) { expect(isReadonly(value)).toBe(true) } }) @@ -232,13 +236,14 @@ describe('reactivity/readonly', () => { const key1 = {} const key2 = {} const original = new Collection([key1, key2]) - const observed = readonly(original) - expect(observed).not.toBe(original) - expect(isReactive(observed)).toBe(true) - expect(isReadonly(observed)).toBe(true) + const wrapped = readonly(original) + expect(wrapped).not.toBe(original) + expect(isProxy(wrapped)).toBe(true) + expect(isReactive(wrapped)).toBe(false) + expect(isReadonly(wrapped)).toBe(true) expect(isReactive(original)).toBe(false) expect(isReadonly(original)).toBe(false) - expect(observed.has(reactive(key1))).toBe(true) + expect(wrapped.has(reactive(key1))).toBe(true) expect(original.has(reactive(key1))).toBe(false) }) @@ -261,17 +266,17 @@ describe('reactivity/readonly', () => { if (Collection === Set) { test('should retrieve readonly values on iteration', () => { const original = new Collection([{}, {}]) - const observed: any = readonly(original) - for (const value of observed) { + const wrapped: any = readonly(original) + for (const value of wrapped) { expect(isReadonly(value)).toBe(true) } - observed.forEach((value: any) => { + wrapped.forEach((value: any) => { expect(isReadonly(value)).toBe(true) }) - for (const value of observed.values()) { + for (const value of wrapped.values()) { expect(isReadonly(value)).toBe(true) } - for (const [v1, v2] of observed.entries()) { + for (const [v1, v2] of wrapped.entries()) { expect(isReadonly(v1)).toBe(true) expect(isReadonly(v2)).toBe(true) } @@ -299,6 +304,9 @@ describe('reactivity/readonly', () => { test('readonly should track and trigger if wrapping reactive original', () => { const a = reactive({ n: 1 }) const b = readonly(a) + // should return true since it's wrapping a reactive source + expect(isReactive(b)).toBe(true) + let dummy effect(() => { dummy = b.n @@ -309,26 +317,26 @@ describe('reactivity/readonly', () => { expect(dummy).toBe(2) }) - test('observing already observed value should return same Proxy', () => { + test('wrapping already wrapped value should return same Proxy', () => { const original = { foo: 1 } - const observed = readonly(original) - const observed2 = readonly(observed) - expect(observed2).toBe(observed) + const wrapped = readonly(original) + const wrapped2 = readonly(wrapped) + expect(wrapped2).toBe(wrapped) }) - test('observing the same value multiple times should return same Proxy', () => { + test('wrapping the same value multiple times should return same Proxy', () => { const original = { foo: 1 } - const observed = readonly(original) - const observed2 = readonly(original) - expect(observed2).toBe(observed) + const wrapped = readonly(original) + const wrapped2 = readonly(original) + expect(wrapped2).toBe(wrapped) }) - test('markNonReactive', () => { + test('markRaw', () => { const obj = readonly({ foo: { a: 1 }, - bar: markNonReactive({ b: 2 }) + bar: markRaw({ b: 2 }) }) - expect(isReactive(obj.foo)).toBe(true) + expect(isReadonly(obj.foo)).toBe(true) expect(isReactive(obj.bar)).toBe(false) }) diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 280b09c5..ea0a9592 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -11,13 +11,14 @@ export { } from './ref' export { reactive, - isReactive, - shallowReactive, readonly, + isReactive, isReadonly, + isProxy, + shallowReactive, shallowReadonly, - toRaw, - markNonReactive + markRaw, + toRaw } from './reactive' export { computed, diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index be4f8e9b..e6e3c179 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -20,7 +20,7 @@ const readonlyToRaw = new WeakMap() // WeakSets for values that are marked readonly or non-reactive during // observable creation. -const nonReactiveValues = new WeakSet() +const rawValues = new WeakSet() const collectionTypes = new Set([Set, Map, WeakMap, WeakSet]) const isObservableType = /*#__PURE__*/ makeMap( @@ -32,7 +32,7 @@ const canObserve = (value: any): boolean => { !value._isVue && !value._isVNode && isObservableType(toRawType(value)) && - !nonReactiveValues.has(value) && + !rawValues.has(value) && !Object.isFrozen(value) ) } @@ -132,19 +132,24 @@ function createReactiveObject( } export function isReactive(value: unknown): boolean { - return reactiveToRaw.has(value) || readonlyToRaw.has(value) + value = readonlyToRaw.get(value) || value + return reactiveToRaw.has(value) } export function isReadonly(value: unknown): boolean { return readonlyToRaw.has(value) } +export function isProxy(value: unknown): boolean { + return readonlyToRaw.has(value) || reactiveToRaw.has(value) +} + export function toRaw(observed: T): T { observed = readonlyToRaw.get(observed) || observed return reactiveToRaw.get(observed) || observed } -export function markNonReactive(value: T): T { - nonReactiveValues.add(value) +export function markRaw(value: T): T { + rawValues.add(value) return value } diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index d2898673..39d7414a 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -1,7 +1,7 @@ import { track, trigger } from './effect' import { TrackOpTypes, TriggerOpTypes } from './operations' import { isObject } from '@vue/shared' -import { reactive, isReactive } from './reactive' +import { reactive, isProxy } from './reactive' import { ComputedRef } from './computed' import { CollectionTypes } from './collectionHandlers' @@ -98,7 +98,7 @@ export function customRef(factory: CustomRefFactory): Ref { export function toRefs( object: T ): { [K in keyof T]: Ref } { - if (__DEV__ && !isReactive(object)) { + if (__DEV__ && !isProxy(object)) { console.warn(`toRefs() expects a reactive object but received a plain one.`) } const ret: any = {} diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index ee773d93..b4148056 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -19,7 +19,7 @@ import { import { warn } from './warning' import { Data, ComponentInternalInstance } from './component' import { isEmitListener } from './componentEmits' -import { InternalObjectSymbol } from './vnode' +import { InternalObjectKey } from './vnode' export type ComponentPropsOptions

= | ComponentObjectPropsOptions

@@ -104,7 +104,7 @@ export function initProps( ) { const props: Data = {} const attrs: Data = {} - def(attrs, InternalObjectSymbol, true) + def(attrs, InternalObjectKey, 1) setFullProps(instance, rawProps, props, attrs) const options = instance.type.props // validation diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index e3c23f88..6ed3ef0f 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -4,7 +4,7 @@ import { VNodeNormalizedChildren, normalizeVNode, VNodeChild, - InternalObjectSymbol + InternalObjectKey } from './vnode' import { isArray, @@ -111,7 +111,7 @@ export const initSlots = ( normalizeVNodeSlots(instance, children) } } - def(instance.slots, InternalObjectSymbol, true) + def(instance.slots, InternalObjectKey, 1) } export const updateSlots = ( diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index be87de0f..5a855e37 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -2,20 +2,24 @@ export const version = __VERSION__ export { + // core + reactive, ref, + readonly, + // utilities unref, - shallowRef, isRef, toRef, toRefs, - customRef, - reactive, + isProxy, isReactive, - readonly, isReadonly, + // advanced + customRef, + shallowRef, shallowReactive, shallowReadonly, - markNonReactive, + markRaw, toRaw } from '@vue/reactivity' export { computed } from './apiComputed' diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index aeede390..2277d9fd 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -17,7 +17,7 @@ import { ClassComponent } from './component' import { RawSlots } from './componentSlots' -import { isReactive, Ref, toRaw } from '@vue/reactivity' +import { isProxy, Ref, toRaw } from '@vue/reactivity' import { AppContext } from './apiCreateApp' import { SuspenseImpl, @@ -234,7 +234,7 @@ const createVNodeWithArgsTransform = ( ) } -export const InternalObjectSymbol = Symbol() +export const InternalObjectKey = `__vInternal` export const createVNode = (__DEV__ ? createVNodeWithArgsTransform @@ -262,7 +262,7 @@ function _createVNode( // class & style normalization. if (props) { // for reactive or proxy objects, we need to clone it to enable mutation. - if (isReactive(props) || InternalObjectSymbol in props) { + if (isProxy(props) || InternalObjectKey in props) { props = extend({}, props) } let { class: klass, style } = props @@ -272,7 +272,7 @@ function _createVNode( if (isObject(style)) { // reactive state objects need to be cloned since they are likely to be // mutated - if (isReactive(style) && !isArray(style)) { + if (isProxy(style) && !isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) @@ -292,16 +292,12 @@ function _createVNode( ? ShapeFlags.FUNCTIONAL_COMPONENT : 0 - if ( - __DEV__ && - shapeFlag & ShapeFlags.STATEFUL_COMPONENT && - isReactive(type) - ) { + if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) { type = toRaw(type) warn( `Vue received a Component which was made a reactive object. This can ` + `lead to unnecessary performance overhead, and should be avoided by ` + - `marking the component with \`markNonReactive\` or using \`shallowRef\` ` + + `marking the component with \`markRaw\` or using \`shallowRef\` ` + `instead of \`ref\`.`, `\nComponent that was made reactive: `, type @@ -454,7 +450,7 @@ export function normalizeChildren(vnode: VNode, children: unknown) { return } else { type = ShapeFlags.SLOTS_CHILDREN - if (!(children as RawSlots)._ && !(InternalObjectSymbol in children!)) { + if (!(children as RawSlots)._ && !(InternalObjectKey in children!)) { // if slots are not normalized, attach context instance // (compiled / normalized slots already have context) ;(children as RawSlots)._ctx = currentRenderingInstance diff --git a/packages/runtime-test/src/nodeOps.ts b/packages/runtime-test/src/nodeOps.ts index d86fdeaf..a7527115 100644 --- a/packages/runtime-test/src/nodeOps.ts +++ b/packages/runtime-test/src/nodeOps.ts @@ -1,4 +1,4 @@ -import { markNonReactive } from '@vue/reactivity' +import { markRaw } from '@vue/reactivity' export const enum NodeTypes { TEXT = 'text', @@ -88,7 +88,7 @@ function createElement(tag: string): TestElement { tag }) // avoid test nodes from being observed - markNonReactive(node) + markRaw(node) return node } @@ -106,7 +106,7 @@ function createText(text: string): TestText { text }) // avoid test nodes from being observed - markNonReactive(node) + markRaw(node) return node } @@ -124,7 +124,7 @@ function createComment(text: string): TestComment { text }) // avoid test nodes from being observed - markNonReactive(node) + markRaw(node) return node }