From a35e2970b25d4792f094b585cc20d15dd01f0262 Mon Sep 17 00:00:00 2001 From: Claudio Eyzaguirre Date: Mon, 13 Apr 2020 14:43:57 -0400 Subject: [PATCH 01/16] chore: fix typo in Chagelog.md (#960) [ci skip] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 792f1400e..faeae81c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ * **compiler:** compiler options have been adjusted. - new option `decodeEntities` is added. - `namedCharacterReferences` option has been removed. - - `maxCRNameLength` option has been rmeoved. + - `maxCRNameLength` option has been removed. * **asyncComponent:** `retryWhen` and `maxRetries` options for `defineAsyncComponent` has been replaced by the more flexible `onError` option, per https://github.com/vuejs/rfcs/pull/148 From 9e9d2644127a17f770f325d1f1c88b12a34c8789 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 13 Apr 2020 17:13:06 -0400 Subject: [PATCH 02/16] Revert "feat(reactivity): add effect to public api (#909)" (#961) This reverts commit 6fba2418507d9c65891e8d14bd63736adb377556. --- packages/runtime-core/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 4cd04f4db..7e4ad2f70 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -2,7 +2,6 @@ export const version = __VERSION__ export { - effect, ref, unref, shallowRef, From e8a866ec9945ec0464035be4c4c58d6212080a50 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 13 Apr 2020 17:39:48 -0400 Subject: [PATCH 03/16] refactor(reactivity): remove stale API `markReadonly` BREAKING CHANGE: `markReadonly` has been removed. --- packages/reactivity/__tests__/readonly.spec.ts | 12 ------------ packages/reactivity/src/index.ts | 1 - packages/reactivity/src/reactive.ts | 10 ---------- packages/runtime-core/src/index.ts | 1 - 4 files changed, 24 deletions(-) diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts index e009022d0..0138b7ebd 100644 --- a/packages/reactivity/__tests__/readonly.spec.ts +++ b/packages/reactivity/__tests__/readonly.spec.ts @@ -5,7 +5,6 @@ import { isReactive, isReadonly, markNonReactive, - markReadonly, lock, unlock, effect, @@ -424,17 +423,6 @@ describe('reactivity/readonly', () => { expect(isReactive(obj.bar)).toBe(false) }) - test('markReadonly', () => { - const obj = reactive({ - foo: { a: 1 }, - bar: markReadonly({ b: 2 }) - }) - expect(isReactive(obj.foo)).toBe(true) - expect(isReactive(obj.bar)).toBe(true) - expect(isReadonly(obj.foo)).toBe(false) - expect(isReadonly(obj.bar)).toBe(true) - }) - test('should make ref readonly', () => { const n: any = readonly(ref(1)) n.value = 2 diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 21a9eae8f..91b1829e0 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -7,7 +7,6 @@ export { isReadonly, shallowReadonly, toRaw, - markReadonly, markNonReactive } from './reactive' export { diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index bc1b72524..d6f9c67ad 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -20,7 +20,6 @@ const readonlyToRaw = new WeakMap() // WeakSets for values that are marked readonly or non-reactive during // observable creation. -const readonlyValues = new WeakSet() const nonReactiveValues = new WeakSet() const collectionTypes = new Set([Set, Map, WeakMap, WeakSet]) @@ -47,10 +46,6 @@ export function reactive(target: object) { if (readonlyToRaw.has(target)) { return target } - // target is explicitly marked as readonly by user - if (readonlyValues.has(target)) { - return readonly(target) - } if (isRef(target)) { return target } @@ -156,11 +151,6 @@ export function toRaw(observed: T): T { return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed } -export function markReadonly(value: T): T { - readonlyValues.add(value) - return value -} - export function markNonReactive(value: T): T { nonReactiveValues.add(value) return value diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 7e4ad2f70..334b7c6f3 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -13,7 +13,6 @@ export { isReadonly, shallowReactive, toRaw, - markReadonly, markNonReactive } from '@vue/reactivity' export { computed } from './apiComputed' From 0869443d018fd81e8f0095e1a0a032b05b1260fa Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Apr 2020 09:59:02 -0400 Subject: [PATCH 04/16] chore: import toRaw from @vue/reactivity --- packages/runtime-dom/src/components/TransitionGroup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-dom/src/components/TransitionGroup.ts b/packages/runtime-dom/src/components/TransitionGroup.ts index 24d23b9e7..018364411 100644 --- a/packages/runtime-dom/src/components/TransitionGroup.ts +++ b/packages/runtime-dom/src/components/TransitionGroup.ts @@ -13,7 +13,6 @@ import { VNode, warn, resolveTransitionHooks, - toRaw, useTransitionState, getCurrentInstance, setTransitionHooks, @@ -21,6 +20,7 @@ import { onUpdated, SetupContext } from '@vue/runtime-core' +import { toRaw } from '@vue/reactivity' interface Position { top: number From c80b857eb5b19f48f498147479a779af9953be32 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Apr 2020 16:17:35 -0400 Subject: [PATCH 05/16] fix(runtime-core): should resolve value instead of delete for dynamic props with options --- .../__tests__/componentProps.spec.ts | 10 +++- packages/runtime-core/src/componentProps.ts | 48 +++++++++++-------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/packages/runtime-core/__tests__/componentProps.spec.ts b/packages/runtime-core/__tests__/componentProps.spec.ts index 402fc0d62..9e8299ffc 100644 --- a/packages/runtime-core/__tests__/componentProps.spec.ts +++ b/packages/runtime-core/__tests__/componentProps.spec.ts @@ -175,9 +175,17 @@ describe('component props', () => { expect(proxy.foo).toBe(2) expect(proxy.bar).toEqual({ a: 1 }) - render(h(Comp, { foo: undefined, bar: { b: 2 } }), root) + render(h(Comp, { bar: { b: 2 } }), root) expect(proxy.foo).toBe(1) expect(proxy.bar).toEqual({ b: 2 }) + + render(h(Comp, { foo: 3, bar: { b: 3 } }), root) + expect(proxy.foo).toBe(3) + expect(proxy.bar).toEqual({ b: 3 }) + + render(h(Comp, { bar: { b: 4 } }), root) + expect(proxy.foo).toBe(1) + expect(proxy.bar).toEqual({ b: 4 }) }) test('optimized props updates', async () => { diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index 0ca0dbaa0..5b871076a 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -186,7 +186,16 @@ export function updateProps( // and converted to camelCase (#955) ((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey))) ) { - delete props[key] + if (options) { + props[key] = resolvePropValue( + options, + rawProps || EMPTY_OBJ, + key, + undefined + ) + } else { + delete props[key] + } } } for (const key in attrs) { @@ -250,25 +259,24 @@ function resolvePropValue( key: string, value: unknown ) { - let opt = options[key] - if (opt == null) { - return value - } - const hasDefault = hasOwn(opt, 'default') - // default values - if (hasDefault && value === undefined) { - const defaultValue = opt.default - value = isFunction(defaultValue) ? defaultValue() : defaultValue - } - // boolean casting - if (opt[BooleanFlags.shouldCast]) { - if (!hasOwn(props, key) && !hasDefault) { - value = false - } else if ( - opt[BooleanFlags.shouldCastTrue] && - (value === '' || value === hyphenate(key)) - ) { - value = true + const opt = options[key] + if (opt != null) { + const hasDefault = hasOwn(opt, 'default') + // default values + if (hasDefault && value === undefined) { + const defaultValue = opt.default + value = isFunction(defaultValue) ? defaultValue() : defaultValue + } + // boolean casting + if (opt[BooleanFlags.shouldCast]) { + if (!hasOwn(props, key) && !hasDefault) { + value = false + } else if ( + opt[BooleanFlags.shouldCastTrue] && + (value === '' || value === hyphenate(key)) + ) { + value = true + } } } return value From 78977c399734da7c4f8d58f2ccd650533e89249f Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Apr 2020 17:31:35 -0400 Subject: [PATCH 06/16] fix(scheduler): sort jobs before flushing This fixes the case where a child component is added to the queue before its parent, but should be invalidated by its parent's update. Same logic was present in Vue 2. Properly fixes #910 ref: https://github.com/vuejs/vue-next/issues/910#issuecomment-613097539 --- packages/reactivity/src/effect.ts | 6 +++- .../runtime-core/__tests__/scheduler.spec.ts | 16 +++++++++ packages/runtime-core/src/scheduler.ts | 33 ++++++++++++++----- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 6a09640e8..aab4ae6db 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -12,6 +12,7 @@ const targetMap = new WeakMap() export interface ReactiveEffect { (...args: any[]): T _isEffect: true + id: number active: boolean raw: () => T deps: Array @@ -21,7 +22,7 @@ export interface ReactiveEffect { export interface ReactiveEffectOptions { lazy?: boolean computed?: boolean - scheduler?: (job: () => void) => void + scheduler?: (job: ReactiveEffect) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void @@ -74,6 +75,8 @@ export function stop(effect: ReactiveEffect) { } } +let uid = 0 + function createReactiveEffect( fn: (...args: any[]) => T, options: ReactiveEffectOptions @@ -96,6 +99,7 @@ function createReactiveEffect( } } } as ReactiveEffect + effect.id = uid++ effect._isEffect = true effect.active = true effect.raw = fn diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts index cf035c97a..a4554325a 100644 --- a/packages/runtime-core/__tests__/scheduler.spec.ts +++ b/packages/runtime-core/__tests__/scheduler.spec.ts @@ -262,4 +262,20 @@ describe('scheduler', () => { // job2 should be called only once expect(calls).toEqual(['job1', 'job2', 'job3', 'job4']) }) + + test('sort job based on id', async () => { + const calls: string[] = [] + const job1 = () => calls.push('job1') + // job1 has no id + const job2 = () => calls.push('job2') + job2.id = 2 + const job3 = () => calls.push('job3') + job3.id = 1 + + queueJob(job1) + queueJob(job2) + queueJob(job3) + await nextTick() + expect(calls).toEqual(['job3', 'job2', 'job1']) + }) }) diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index c730730f4..e518427d2 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -1,7 +1,12 @@ import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { isArray } from '@vue/shared' -const queue: (Function | null)[] = [] +export interface Job { + (): void + id?: number +} + +const queue: (Job | null)[] = [] const postFlushCbs: Function[] = [] const p = Promise.resolve() @@ -9,20 +14,20 @@ let isFlushing = false let isFlushPending = false const RECURSION_LIMIT = 100 -type CountMap = Map +type CountMap = Map export function nextTick(fn?: () => void): Promise { return fn ? p.then(fn) : p } -export function queueJob(job: () => void) { +export function queueJob(job: Job) { if (!queue.includes(job)) { queue.push(job) queueFlush() } } -export function invalidateJob(job: () => void) { +export function invalidateJob(job: Job) { const i = queue.indexOf(job) if (i > -1) { queue[i] = null @@ -45,11 +50,9 @@ function queueFlush() { } } -const dedupe = (cbs: Function[]): Function[] => [...new Set(cbs)] - export function flushPostFlushCbs(seen?: CountMap) { if (postFlushCbs.length) { - const cbs = dedupe(postFlushCbs) + const cbs = [...new Set(postFlushCbs)] postFlushCbs.length = 0 if (__DEV__) { seen = seen || new Map() @@ -63,6 +66,8 @@ export function flushPostFlushCbs(seen?: CountMap) { } } +const getId = (job: Job) => (job.id == null ? Infinity : job.id) + function flushJobs(seen?: CountMap) { isFlushPending = false isFlushing = true @@ -70,6 +75,18 @@ function flushJobs(seen?: CountMap) { if (__DEV__) { seen = seen || new Map() } + + // Sort queue before flush. + // This ensures that: + // 1. Components are updated from parent to child. (because parent is always + // created before the child so its render effect will have smaller + // priority number) + // 2. If a component is unmounted during a parent component's update, + // its update can be skipped. + // Jobs can never be null before flush starts, since they are only invalidated + // during execution of another flushed job. + queue.sort((a, b) => getId(a!) - getId(b!)) + while ((job = queue.shift()) !== undefined) { if (job === null) { continue @@ -88,7 +105,7 @@ function flushJobs(seen?: CountMap) { } } -function checkRecursiveUpdates(seen: CountMap, fn: Function) { +function checkRecursiveUpdates(seen: CountMap, fn: Job | Function) { if (!seen.has(fn)) { seen.set(fn, 1) } else { From 3e7bb7d110818d7b90ca4acc47afc30508f465b7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Apr 2020 17:40:41 -0400 Subject: [PATCH 07/16] feat(runtime-core): warn async data() --- packages/runtime-core/src/componentOptions.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index ffd086983..31bb01ad0 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -15,7 +15,8 @@ import { isArray, EMPTY_OBJ, NOOP, - hasOwn + hasOwn, + isPromise } from '@vue/shared' import { computed } from './apiComputed' import { watch, WatchOptions, WatchCallback } from './apiWatch' @@ -316,6 +317,13 @@ export function applyOptions( ) } const data = dataOptions.call(ctx, ctx) + if (__DEV__ && isPromise(data)) { + warn( + `data() returned a Promise - note data() cannot be async; If you ` + + `intend to perform data fetching before component renders, use ` + + `async setup() + .` + ) + } if (!isObject(data)) { __DEV__ && warn(`data() should return an object.`) } else if (instance.data === EMPTY_OBJ) { From 2e06f5bbe84155588dea82d90822a41dc93d0688 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Apr 2020 18:07:47 -0400 Subject: [PATCH 08/16] feat(runtime-core): detect and warn against components made reactive close #962 --- packages/runtime-core/src/vnode.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index ef6f3ab85..1bb9b4d72 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 } from '@vue/reactivity' +import { isReactive, Ref, toRaw } from '@vue/reactivity' import { AppContext } from './apiCreateApp' import { SuspenseImpl, @@ -236,7 +236,7 @@ const createVNodeWithArgsTransform = ( export const InternalObjectSymbol = Symbol() -export const createVNode = (__DEV__ +export const createVNode = (false ? createVNodeWithArgsTransform : _createVNode) as typeof _createVNode @@ -292,6 +292,22 @@ function _createVNode( ? ShapeFlags.FUNCTIONAL_COMPONENT : 0 + if ( + __DEV__ && + shapeFlag & ShapeFlags.STATEFUL_COMPONENT && + isReactive(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\` ` + + `instead of \`ref\`.`, + `\nComponent that was made reactive: `, + type + ) + } + const vnode: VNode = { _isVNode: true, type, From 8ae362400e8c259364ccd91f17b2887bb3b55a0b Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Apr 2020 18:13:53 -0400 Subject: [PATCH 09/16] types: fix public instance $root and $parent type --- packages/runtime-core/src/componentProxy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index f30c300a3..9e28d2659 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -36,8 +36,8 @@ export type ComponentPublicInstance< $attrs: Data $refs: Data $slots: Slots - $root: ComponentInternalInstance | null - $parent: ComponentInternalInstance | null + $root: ComponentPublicInstance | null + $parent: ComponentPublicInstance | null $emit: EmitFn $el: any $options: ComponentOptionsBase From 4046f0bc037173f01c34f7833e9dd7883dafe911 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Apr 2020 18:32:27 -0400 Subject: [PATCH 10/16] chore: revert debugging dev flag --- packages/runtime-core/src/vnode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 1bb9b4d72..aeede390b 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -236,7 +236,7 @@ const createVNodeWithArgsTransform = ( export const InternalObjectSymbol = Symbol() -export const createVNode = (false +export const createVNode = (__DEV__ ? createVNodeWithArgsTransform : _createVNode) as typeof _createVNode From b83c5801315e5e28ac51ecff743206e665f4d868 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Apr 2020 20:45:41 -0400 Subject: [PATCH 11/16] feat(reactivity): add support for `customRef` API --- packages/reactivity/__tests__/ref.spec.ts | 31 ++++++++++++++++++++++- packages/reactivity/src/index.ts | 11 +++++++- packages/reactivity/src/ref.ts | 25 ++++++++++++++++++ packages/runtime-core/src/index.ts | 5 ++-- 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index 6620f7fd9..9ebe8f46e 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -8,7 +8,7 @@ import { isReactive } from '../src/index' import { computed } from '@vue/runtime-dom' -import { shallowRef, unref } from '../src/ref' +import { shallowRef, unref, customRef } from '../src/ref' describe('reactivity/ref', () => { it('should hold a value', () => { @@ -208,4 +208,33 @@ describe('reactivity/ref', () => { expect(dummyX).toBe(4) expect(dummyY).toBe(5) }) + + test('customRef', () => { + let value = 1 + let _trigger: () => void + + const custom = customRef((track, trigger) => ({ + get() { + track() + return value + }, + set(newValue: number) { + value = newValue + _trigger = trigger + } + })) + + let dummy + effect(() => { + dummy = custom.value + }) + expect(dummy).toBe(1) + + custom.value = 2 + // should not trigger yet + expect(dummy).toBe(1) + + _trigger!() + expect(dummy).toBe(2) + }) }) diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 91b1829e0..d83b0f305 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -1,4 +1,13 @@ -export { ref, unref, shallowRef, isRef, toRefs, Ref, UnwrapRef } from './ref' +export { + ref, + unref, + shallowRef, + isRef, + toRefs, + customRef, + Ref, + UnwrapRef +} from './ref' export { reactive, isReactive, diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 05304ad7e..bbbd94841 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -70,6 +70,31 @@ export function unref(ref: T): T extends Ref ? V : T { return isRef(ref) ? (ref.value as any) : ref } +export type CustomRefFactory = ( + track: () => void, + trigger: () => void +) => { + get: () => T + set: (value: T) => void +} + +export function customRef(factory: CustomRefFactory): Ref { + const { get, set } = factory( + () => track(r, TrackOpTypes.GET, 'value'), + () => trigger(r, TriggerOpTypes.SET, 'value') + ) + const r = { + _isRef: true, + get value() { + return get() + }, + set value(v) { + set(v) + } + } + return r as any +} + export function toRefs( object: T ): { [K in keyof T]: Ref } { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 334b7c6f3..2b4923e23 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -7,13 +7,14 @@ export { shallowRef, isRef, toRefs, + customRef, reactive, isReactive, readonly, isReadonly, shallowReactive, - toRaw, - markNonReactive + markNonReactive, + toRaw } from '@vue/reactivity' export { computed } from './apiComputed' export { watch, watchEffect } from './apiWatch' From 486dc188fe1593448d2bfb0c3c4c3c02b2d78ea4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Apr 2020 20:49:18 -0400 Subject: [PATCH 12/16] feat(reactivity): add support for `toRef` API --- packages/reactivity/__tests__/ref.spec.ts | 31 +++++++++++++++++++++++ packages/reactivity/src/index.ts | 1 + packages/reactivity/src/ref.ts | 4 +-- packages/runtime-core/src/index.ts | 1 + 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index 9ebe8f46e..27c7a0b26 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -3,6 +3,7 @@ import { effect, reactive, isRef, + toRef, toRefs, Ref, isReactive @@ -168,6 +169,34 @@ describe('reactivity/ref', () => { expect(isRef({ value: 0 })).toBe(false) }) + test('toRef', () => { + const a = reactive({ + x: 1 + }) + const x = toRef(a, 'x') + expect(isRef(x)).toBe(true) + expect(x.value).toBe(1) + + // source -> proxy + a.x = 2 + expect(x.value).toBe(2) + + // proxy -> source + x.value = 3 + expect(a.x).toBe(3) + + // reactivity + let dummyX + effect(() => { + dummyX = x.value + }) + expect(dummyX).toBe(x.value) + + // mutating source should trigger effect using the proxy refs + a.x = 4 + expect(dummyX).toBe(4) + }) + test('toRefs', () => { const a = reactive({ x: 1, @@ -224,6 +253,8 @@ describe('reactivity/ref', () => { } })) + expect(isRef(custom)).toBe(true) + let dummy effect(() => { dummy = custom.value diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index d83b0f305..0a1737317 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -3,6 +3,7 @@ export { unref, shallowRef, isRef, + toRef, toRefs, customRef, Ref, diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index bbbd94841..8338ffab2 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -103,12 +103,12 @@ export function toRefs( } const ret: any = {} for (const key in object) { - ret[key] = toProxyRef(object, key) + ret[key] = toRef(object, key) } return ret } -function toProxyRef( +export function toRef( object: T, key: K ): Ref { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 2b4923e23..c1cf0c5d1 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -6,6 +6,7 @@ export { unref, shallowRef, isRef, + toRef, toRefs, customRef, reactive, From 09b44e07cbfd75f34e26e3ffee6a06a200acda88 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Apr 2020 22:18:58 -0400 Subject: [PATCH 13/16] refactor(reactivity): move array ref handling into getter --- packages/reactivity/src/baseHandlers.ts | 31 +++++++++++++++---------- packages/reactivity/src/reactive.ts | 5 +--- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 4d5ec50f7..8cda6d76b 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -36,7 +36,8 @@ const arrayInstrumentations: Record = {} function createGetter(isReadonly = false, shallow = false) { return function get(target: object, key: string | symbol, receiver: object) { - if (isArray(target) && hasOwn(arrayInstrumentations, key)) { + const targetIsArray = isArray(target) + if (targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } const res = Reflect.get(target, key, receiver) @@ -48,18 +49,24 @@ function createGetter(isReadonly = false, shallow = false) { // TODO strict mode that returns a shallow-readonly version of the value return res } - // ref unwrapping, only for Objects, not for Arrays. - if (isRef(res) && !isArray(target)) { - return res.value + if (isRef(res)) { + if (targetIsArray) { + track(target, TrackOpTypes.GET, key) + return res + } else { + // ref unwrapping, only for Objects, not for Arrays. + return res.value + } + } else { + track(target, TrackOpTypes.GET, key) + return isObject(res) + ? isReadonly + ? // need to lazy access readonly and reactive here to avoid + // circular dependency + readonly(res) + : reactive(res) + : res } - track(target, TrackOpTypes.GET, key) - return isObject(res) - ? isReadonly - ? // need to lazy access readonly and reactive here to avoid - // circular dependency - readonly(res) - : reactive(res) - : res } } diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index d6f9c67ad..8ec53aff0 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -9,7 +9,7 @@ import { mutableCollectionHandlers, readonlyCollectionHandlers } from './collectionHandlers' -import { UnwrapRef, Ref, isRef } from './ref' +import { UnwrapRef, Ref } from './ref' import { makeMap } from '@vue/shared' // WeakMaps that store {raw <-> observed} pairs. @@ -46,9 +46,6 @@ export function reactive(target: object) { if (readonlyToRaw.has(target)) { return target } - if (isRef(target)) { - return target - } return createReactiveObject( target, rawToReactive, From 317850427337cca1cc5998a03c110555903121d3 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Apr 2020 23:49:46 -0400 Subject: [PATCH 14/16] refactor(reactivity): make readonly non-tracking --- .../reactivity/__tests__/readonly.spec.ts | 129 ++---------------- packages/reactivity/src/baseHandlers.ts | 82 +++++------ packages/reactivity/src/collectionHandlers.ts | 45 +++--- packages/reactivity/src/index.ts | 1 - packages/reactivity/src/lock.ts | 10 -- packages/reactivity/src/reactive.ts | 38 +++--- .../__tests__/componentProxy.spec.ts | 11 +- packages/runtime-core/src/component.ts | 18 +-- packages/runtime-core/src/componentProps.ts | 10 +- packages/runtime-core/src/componentProxy.ts | 15 +- packages/runtime-core/src/index.ts | 1 + 11 files changed, 110 insertions(+), 250 deletions(-) delete mode 100644 packages/reactivity/src/lock.ts diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts index 0138b7ebd..0f5b23bec 100644 --- a/packages/reactivity/__tests__/readonly.spec.ts +++ b/packages/reactivity/__tests__/readonly.spec.ts @@ -5,8 +5,6 @@ import { isReactive, isReadonly, markNonReactive, - lock, - unlock, effect, ref, shallowReadonly @@ -90,22 +88,7 @@ describe('reactivity/readonly', () => { ).toHaveBeenWarnedLast() }) - it('should allow mutation when unlocked', () => { - const observed: any = readonly({ foo: 1, bar: { baz: 2 } }) - unlock() - observed.prop = 2 - observed.bar.qux = 3 - delete observed.bar.baz - delete observed.foo - lock() - expect(observed.prop).toBe(2) - expect(observed.foo).toBeUndefined() - expect(observed.bar.qux).toBe(3) - expect('baz' in observed.bar).toBe(false) - expect(`target is readonly`).not.toHaveBeenWarned() - }) - - it('should not trigger effects when locked', () => { + it('should not trigger effects', () => { const observed: any = readonly({ a: 1 }) let dummy effect(() => { @@ -117,20 +100,6 @@ describe('reactivity/readonly', () => { expect(dummy).toBe(1) expect(`target is readonly`).toHaveBeenWarned() }) - - it('should trigger effects when unlocked', () => { - const observed: any = readonly({ a: 1 }) - let dummy - effect(() => { - dummy = observed.a - }) - expect(dummy).toBe(1) - unlock() - observed.a = 2 - lock() - expect(observed.a).toBe(2) - expect(dummy).toBe(2) - }) }) describe('Array', () => { @@ -182,23 +151,7 @@ describe('reactivity/readonly', () => { expect(`target is readonly.`).toHaveBeenWarnedTimes(5) }) - it('should allow mutation when unlocked', () => { - const observed: any = readonly([{ foo: 1, bar: { baz: 2 } }]) - unlock() - observed[1] = 2 - observed.push(3) - observed[0].foo = 2 - observed[0].bar.baz = 3 - lock() - expect(observed.length).toBe(3) - expect(observed[1]).toBe(2) - expect(observed[2]).toBe(3) - expect(observed[0].foo).toBe(2) - expect(observed[0].bar.baz).toBe(3) - expect(`target is readonly`).not.toHaveBeenWarned() - }) - - it('should not trigger effects when locked', () => { + it('should not trigger effects', () => { const observed: any = readonly([{ a: 1 }]) let dummy effect(() => { @@ -214,30 +167,6 @@ describe('reactivity/readonly', () => { expect(dummy).toBe(1) expect(`target is readonly`).toHaveBeenWarnedTimes(2) }) - - it('should trigger effects when unlocked', () => { - const observed: any = readonly([{ a: 1 }]) - let dummy - effect(() => { - dummy = observed[0].a - }) - expect(dummy).toBe(1) - - unlock() - - observed[0].a = 2 - expect(observed[0].a).toBe(2) - expect(dummy).toBe(2) - - observed[0] = { a: 3 } - expect(observed[0].a).toBe(3) - expect(dummy).toBe(3) - - observed.unshift({ a: 4 }) - expect(observed[0].a).toBe(4) - expect(dummy).toBe(4) - lock() - }) }) const maps = [Map, WeakMap] @@ -275,23 +204,6 @@ describe('reactivity/readonly', () => { ).toHaveBeenWarned() }) - test('should allow mutation & trigger effect when unlocked', () => { - const map = readonly(new Collection()) - const isWeak = Collection === WeakMap - const key = {} - let dummy - effect(() => { - dummy = map.get(key) + (isWeak ? 0 : map.size) - }) - expect(dummy).toBeNaN() - unlock() - map.set(key, 1) - lock() - expect(dummy).toBe(isWeak ? 1 : 2) - expect(map.get(key)).toBe(1) - expect(`target is readonly`).not.toHaveBeenWarned() - }) - if (Collection === Map) { test('should retrieve readonly values on iteration', () => { const key1 = {} @@ -346,22 +258,6 @@ describe('reactivity/readonly', () => { ).toHaveBeenWarned() }) - test('should allow mutation & trigger effect when unlocked', () => { - const set = readonly(new Collection()) - const key = {} - let dummy - effect(() => { - dummy = set.has(key) - }) - expect(dummy).toBe(false) - unlock() - set.add(key) - lock() - expect(dummy).toBe(true) - expect(set.has(key)).toBe(true) - expect(`target is readonly`).not.toHaveBeenWarned() - }) - if (Collection === Set) { test('should retrieve readonly values on iteration', () => { const original = new Collection([{}, {}]) @@ -400,6 +296,19 @@ describe('reactivity/readonly', () => { expect(toRaw(a)).toBe(toRaw(b)) }) + test('readonly should track and trigger if wrapping reactive original', () => { + const a = reactive({ n: 1 }) + const b = readonly(a) + let dummy + effect(() => { + dummy = b.n + }) + expect(dummy).toBe(1) + a.n++ + expect(b.n).toBe(2) + expect(dummy).toBe(2) + }) + test('observing already observed value should return same Proxy', () => { const original = { foo: 1 } const observed = readonly(original) @@ -458,13 +367,5 @@ describe('reactivity/readonly', () => { `Set operation on key "foo" failed: target is readonly.` ).not.toHaveBeenWarned() }) - - test('should keep reactive properties reactive', () => { - const props: any = shallowReadonly({ n: reactive({ foo: 1 }) }) - unlock() - props.n = reactive({ foo: 2 }) - lock() - expect(isReactive(props.n)).toBe(true) - }) }) }) diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 8cda6d76b..5fc10b027 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -1,7 +1,6 @@ import { reactive, readonly, toRaw } from './reactive' import { TrackOpTypes, TriggerOpTypes } from './operations' import { track, trigger, ITERATE_KEY } from './effect' -import { LOCKED } from './lock' import { isObject, hasOwn, isSymbol, hasChanged, isArray } from '@vue/shared' import { isRef } from './ref' @@ -12,7 +11,7 @@ const builtInSymbols = new Set( ) const get = /*#__PURE__*/ createGetter() -const shallowReactiveGet = /*#__PURE__*/ createGetter(false, true) +const shallowGet = /*#__PURE__*/ createGetter(false, true) const readonlyGet = /*#__PURE__*/ createGetter(true) const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true) @@ -41,57 +40,47 @@ function createGetter(isReadonly = false, shallow = false) { return Reflect.get(arrayInstrumentations, key, receiver) } const res = Reflect.get(target, key, receiver) + if (isSymbol(key) && builtInSymbols.has(key)) { return res } + if (shallow) { - track(target, TrackOpTypes.GET, key) - // TODO strict mode that returns a shallow-readonly version of the value + !isReadonly && track(target, TrackOpTypes.GET, key) return res } + if (isRef(res)) { if (targetIsArray) { - track(target, TrackOpTypes.GET, key) + !isReadonly && track(target, TrackOpTypes.GET, key) return res } else { // ref unwrapping, only for Objects, not for Arrays. return res.value } - } else { - track(target, TrackOpTypes.GET, key) - return isObject(res) - ? isReadonly - ? // need to lazy access readonly and reactive here to avoid - // circular dependency - readonly(res) - : reactive(res) - : res } + + !isReadonly && track(target, TrackOpTypes.GET, key) + return isObject(res) + ? isReadonly + ? // need to lazy access readonly and reactive here to avoid + // circular dependency + readonly(res) + : reactive(res) + : res } } const set = /*#__PURE__*/ createSetter() -const shallowReactiveSet = /*#__PURE__*/ createSetter(false, true) -const readonlySet = /*#__PURE__*/ createSetter(true) -const shallowReadonlySet = /*#__PURE__*/ createSetter(true, true) +const shallowSet = /*#__PURE__*/ createSetter(true) -function createSetter(isReadonly = false, shallow = false) { +function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { - if (isReadonly && LOCKED) { - if (__DEV__) { - console.warn( - `Set operation on key "${String(key)}" failed: target is readonly.`, - target - ) - } - return true - } - const oldValue = (target as any)[key] if (!shallow) { value = toRaw(value) @@ -148,30 +137,32 @@ export const mutableHandlers: ProxyHandler = { export const readonlyHandlers: ProxyHandler = { get: readonlyGet, - set: readonlySet, has, ownKeys, - deleteProperty(target: object, key: string | symbol): boolean { - if (LOCKED) { - if (__DEV__) { - console.warn( - `Delete operation on key "${String( - key - )}" failed: target is readonly.`, - target - ) - } - return true - } else { - return deleteProperty(target, key) + set(target, key) { + if (__DEV__) { + console.warn( + `Set operation on key "${String(key)}" failed: target is readonly.`, + target + ) } + return true + }, + deleteProperty(target, key) { + if (__DEV__) { + console.warn( + `Delete operation on key "${String(key)}" failed: target is readonly.`, + target + ) + } + return true } } export const shallowReactiveHandlers: ProxyHandler = { ...mutableHandlers, - get: shallowReactiveGet, - set: shallowReactiveSet + get: shallowGet, + set: shallowSet } // Props handlers are special in the sense that it should not unwrap top-level @@ -179,6 +170,5 @@ export const shallowReactiveHandlers: ProxyHandler = { // retain the reactivity of the normal readonly object. export const shallowReadonlyHandlers: ProxyHandler = { ...readonlyHandlers, - get: shallowReadonlyGet, - set: shallowReadonlySet + get: shallowReadonlyGet } diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 40e4b476a..6ab7e6fdd 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -1,7 +1,6 @@ import { toRaw, reactive, readonly } from './reactive' import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect' import { TrackOpTypes, TriggerOpTypes } from './operations' -import { LOCKED } from './lock' import { isObject, capitalize, @@ -142,7 +141,7 @@ function createForEach(isReadonly: boolean) { const observed = this const target = toRaw(observed) const wrap = isReadonly ? toReadonly : toReactive - track(target, TrackOpTypes.ITERATE, ITERATE_KEY) + !isReadonly && track(target, TrackOpTypes.ITERATE, ITERATE_KEY) // important: create sure the callback is // 1. invoked with the reactive map as `this` and 3rd arg // 2. the value received should be a corresponding reactive/readonly. @@ -161,11 +160,12 @@ function createIterableMethod(method: string | symbol, isReadonly: boolean) { const isKeyOnly = method === 'keys' && isMap const innerIterator = getProto(target)[method].apply(target, args) const wrap = isReadonly ? toReadonly : toReactive - track( - target, - TrackOpTypes.ITERATE, - isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY - ) + !isReadonly && + track( + target, + TrackOpTypes.ITERATE, + isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY + ) // return a wrapped iterator which returns observed versions of the // values emitted from the real iterator return { @@ -187,23 +187,16 @@ function createIterableMethod(method: string | symbol, isReadonly: boolean) { } } -function createReadonlyMethod( - method: Function, - type: TriggerOpTypes -): Function { +function createReadonlyMethod(type: TriggerOpTypes): Function { return function(this: CollectionTypes, ...args: unknown[]) { - if (LOCKED) { - if (__DEV__) { - const key = args[0] ? `on key "${args[0]}" ` : `` - console.warn( - `${capitalize(type)} operation ${key}failed: target is readonly.`, - toRaw(this) - ) - } - return type === TriggerOpTypes.DELETE ? false : this - } else { - return method.apply(this, args) + if (__DEV__) { + const key = args[0] ? `on key "${args[0]}" ` : `` + console.warn( + `${capitalize(type)} operation ${key}failed: target is readonly.`, + toRaw(this) + ) } + return type === TriggerOpTypes.DELETE ? false : this } } @@ -230,10 +223,10 @@ const readonlyInstrumentations: Record = { return size((this as unknown) as IterableCollections) }, has, - add: createReadonlyMethod(add, TriggerOpTypes.ADD), - set: createReadonlyMethod(set, TriggerOpTypes.SET), - delete: createReadonlyMethod(deleteEntry, TriggerOpTypes.DELETE), - clear: createReadonlyMethod(clear, TriggerOpTypes.CLEAR), + add: createReadonlyMethod(TriggerOpTypes.ADD), + set: createReadonlyMethod(TriggerOpTypes.SET), + delete: createReadonlyMethod(TriggerOpTypes.DELETE), + clear: createReadonlyMethod(TriggerOpTypes.CLEAR), forEach: createForEach(true) } diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 0a1737317..280b09c51 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -40,5 +40,4 @@ export { ReactiveEffectOptions, DebuggerEvent } from './effect' -export { lock, unlock } from './lock' export { TrackOpTypes, TriggerOpTypes } from './operations' diff --git a/packages/reactivity/src/lock.ts b/packages/reactivity/src/lock.ts deleted file mode 100644 index 417526be3..000000000 --- a/packages/reactivity/src/lock.ts +++ /dev/null @@ -1,10 +0,0 @@ -// global immutability lock -export let LOCKED = true - -export function lock() { - LOCKED = true -} - -export function unlock() { - LOCKED = false -} diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index 8ec53aff0..be4f8e9b5 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -2,8 +2,8 @@ import { isObject, toRawType } from '@vue/shared' import { mutableHandlers, readonlyHandlers, - shallowReadonlyHandlers, - shallowReactiveHandlers + shallowReactiveHandlers, + shallowReadonlyHandlers } from './baseHandlers' import { mutableCollectionHandlers, @@ -55,14 +55,22 @@ export function reactive(target: object) { ) } +// Return a reactive-copy of the original object, where only the root level +// properties are reactive, and does NOT unwrap refs nor recursively convert +// returned properties. +export function shallowReactive(target: T): T { + return createReactiveObject( + target, + rawToReactive, + reactiveToRaw, + shallowReactiveHandlers, + mutableCollectionHandlers + ) +} + export function readonly( target: T ): Readonly> { - // value is a mutable observable, retrieve its original and return - // a readonly version. - if (reactiveToRaw.has(target)) { - target = reactiveToRaw.get(target) - } return createReactiveObject( target, rawToReadonly, @@ -88,19 +96,6 @@ export function shallowReadonly( ) } -// Return a reactive-copy of the original object, where only the root level -// properties are reactive, and does NOT unwrap refs nor recursively convert -// returned properties. -export function shallowReactive(target: T): T { - return createReactiveObject( - target, - rawToReactive, - reactiveToRaw, - shallowReactiveHandlers, - mutableCollectionHandlers - ) -} - function createReactiveObject( target: unknown, toProxy: WeakMap, @@ -145,7 +140,8 @@ export function isReadonly(value: unknown): boolean { } export function toRaw(observed: T): T { - return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed + observed = readonlyToRaw.get(observed) || observed + return reactiveToRaw.get(observed) || observed } export function markNonReactive(value: T): T { diff --git a/packages/runtime-core/__tests__/componentProxy.spec.ts b/packages/runtime-core/__tests__/componentProxy.spec.ts index cbe18c2ca..753dbac4b 100644 --- a/packages/runtime-core/__tests__/componentProxy.spec.ts +++ b/packages/runtime-core/__tests__/componentProxy.spec.ts @@ -3,7 +3,8 @@ import { render, getCurrentInstance, nodeOps, - createApp + createApp, + shallowReadonly } from '@vue/runtime-test' import { mockWarn } from '@vue/shared' import { ComponentInternalInstance } from '../src/component' @@ -85,10 +86,10 @@ describe('component: proxy', () => { } render(h(Comp), nodeOps.createElement('div')) expect(instanceProxy.$data).toBe(instance!.data) - expect(instanceProxy.$props).toBe(instance!.props) - expect(instanceProxy.$attrs).toBe(instance!.attrs) - expect(instanceProxy.$slots).toBe(instance!.slots) - expect(instanceProxy.$refs).toBe(instance!.refs) + expect(instanceProxy.$props).toBe(shallowReadonly(instance!.props)) + expect(instanceProxy.$attrs).toBe(shallowReadonly(instance!.attrs)) + expect(instanceProxy.$slots).toBe(shallowReadonly(instance!.slots)) + expect(instanceProxy.$refs).toBe(shallowReadonly(instance!.refs)) expect(instanceProxy.$parent).toBe( instance!.parent && instance!.parent.proxy ) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 50c449661..9bbcc8567 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -3,7 +3,8 @@ import { reactive, ReactiveEffect, pauseTracking, - resetTracking + resetTracking, + shallowReadonly } from '@vue/reactivity' import { ComponentPublicInstance, @@ -347,7 +348,7 @@ function setupStatefulComponent( setup, instance, ErrorCodes.SETUP_FUNCTION, - [instance.props, setupContext] + [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext] ) resetTracking() currentInstance = null @@ -479,17 +480,6 @@ function finishComponentSetup( } } -const slotsHandlers: ProxyHandler = { - set: () => { - warn(`setupContext.slots is readonly.`) - return false - }, - deleteProperty: () => { - warn(`setupContext.slots is readonly.`) - return false - } -} - const attrHandlers: ProxyHandler = { get: (target, key: string) => { markAttrsAccessed() @@ -514,7 +504,7 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext { return new Proxy(instance.attrs, attrHandlers) }, get slots() { - return new Proxy(instance.slots, slotsHandlers) + return shallowReadonly(instance.slots) }, get emit() { return (event: string, ...args: any[]) => instance.emit(event, ...args) diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index 5b871076a..ee773d93f 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -1,4 +1,4 @@ -import { toRaw, lock, unlock, shallowReadonly } from '@vue/reactivity' +import { toRaw, shallowReactive } from '@vue/reactivity' import { EMPTY_OBJ, camelize, @@ -114,7 +114,7 @@ export function initProps( if (isStateful) { // stateful - instance.props = isSSR ? props : shallowReadonly(props) + instance.props = isSSR ? props : shallowReactive(props) } else { if (!options) { // functional w/ optional props, props === attrs @@ -132,9 +132,6 @@ export function updateProps( rawProps: Data | null, optimized: boolean ) { - // allow mutation of propsProxy (which is readonly by default) - unlock() - const { props, attrs, @@ -205,9 +202,6 @@ export function updateProps( } } - // lock readonly - lock() - if (__DEV__ && rawOptions && rawProps) { validateProps(props, rawOptions) } diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 9e28d2659..74c5c5912 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -2,7 +2,12 @@ import { ComponentInternalInstance, Data } from './component' import { nextTick, queueJob } from './scheduler' import { instanceWatch } from './apiWatch' import { EMPTY_OBJ, hasOwn, isGloballyWhitelisted, NOOP } from '@vue/shared' -import { ReactiveEffect, UnwrapRef, toRaw } from '@vue/reactivity' +import { + ReactiveEffect, + UnwrapRef, + toRaw, + shallowReadonly +} from '@vue/reactivity' import { ExtractComputedReturns, ComponentOptionsBase, @@ -57,10 +62,10 @@ const publicPropertiesMap: Record< $: i => i, $el: i => i.vnode.el, $data: i => i.data, - $props: i => i.props, - $attrs: i => i.attrs, - $slots: i => i.slots, - $refs: i => i.refs, + $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props), + $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs), + $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots), + $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs), $parent: i => i.parent && i.parent.proxy, $root: i => i.root && i.root.proxy, $emit: i => i.emit, diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index c1cf0c5d1..be87de0f0 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -14,6 +14,7 @@ export { readonly, isReadonly, shallowReactive, + shallowReadonly, markNonReactive, toRaw } from '@vue/reactivity' From d45e47569d366b932c0e3461afc6478b45a4602d Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 15 Apr 2020 10:35:34 -0400 Subject: [PATCH 15/16] fix(runtime-dom/v-on): support event.stopImmediatePropagation on multiple listeners close #916 --- .../runtime-dom/__tests__/patchEvents.spec.ts | 14 +++++++++++++ packages/runtime-dom/src/modules/events.ts | 20 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/runtime-dom/__tests__/patchEvents.spec.ts b/packages/runtime-dom/__tests__/patchEvents.spec.ts index b870a8be3..2bb8e2ac0 100644 --- a/packages/runtime-dom/__tests__/patchEvents.spec.ts +++ b/packages/runtime-dom/__tests__/patchEvents.spec.ts @@ -134,4 +134,18 @@ describe(`runtime-dom: events patching`, () => { expect(fn).toHaveBeenCalledTimes(1) expect(fn2).toHaveBeenCalledWith(event) }) + + it('should support stopImmediatePropagation on multiple listeners', async () => { + const el = document.createElement('div') + const event = new Event('click') + const fn1 = jest.fn((e: Event) => { + e.stopImmediatePropagation() + }) + const fn2 = jest.fn() + patchProp(el, 'onClick', null, [fn1, fn2]) + el.dispatchEvent(event) + await timeout() + expect(fn1).toHaveBeenCalledTimes(1) + expect(fn2).toHaveBeenCalledTimes(0) + }) }) diff --git a/packages/runtime-dom/src/modules/events.ts b/packages/runtime-dom/src/modules/events.ts index 6789934c7..bb3ae9be0 100644 --- a/packages/runtime-dom/src/modules/events.ts +++ b/packages/runtime-dom/src/modules/events.ts @@ -1,4 +1,4 @@ -import { EMPTY_OBJ } from '@vue/shared' +import { EMPTY_OBJ, isArray } from '@vue/shared' import { ComponentInternalInstance, callWithAsyncErrorHandling @@ -130,7 +130,7 @@ function createInvoker( // AFTER it was attached. if (e.timeStamp >= invoker.lastUpdated - 1) { callWithAsyncErrorHandling( - invoker.value, + patchStopImmediatePropagation(e, invoker.value), instance, ErrorCodes.NATIVE_EVENT_HANDLER, [e] @@ -142,3 +142,19 @@ function createInvoker( invoker.lastUpdated = getNow() return invoker } + +function patchStopImmediatePropagation( + e: Event, + value: EventValue +): EventValue { + if (isArray(value)) { + const originalStop = e.stopImmediatePropagation + e.stopImmediatePropagation = () => { + originalStop.call(e) + ;(e as any)._stopped = true + } + return value.map(fn => (e: Event) => !(e as any)._stopped && fn(e)) + } else { + return value + } +} From 1068212cb2c3e8be89b11fc677a963f94db23708 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 15 Apr 2020 10:51:07 -0400 Subject: [PATCH 16/16] chore: run prettier --- .../__tests__/transforms/vIf.spec.ts | 2 +- packages/compiler-core/src/index.ts | 6 +- packages/runtime-dom/jsx.d.ts | 72 ++++++++++++++----- .../src/helpers/ssrRenderAttrs.ts | 4 +- 4 files changed, 60 insertions(+), 24 deletions(-) diff --git a/packages/compiler-core/__tests__/transforms/vIf.spec.ts b/packages/compiler-core/__tests__/transforms/vIf.spec.ts index 0513ff4e3..0ded2ac20 100644 --- a/packages/compiler-core/__tests__/transforms/vIf.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vIf.spec.ts @@ -78,7 +78,7 @@ describe('compiler: v-if', () => { expect(node.branches[0].children[2].type).toBe(NodeTypes.ELEMENT) expect((node.branches[0].children[2] as ElementNode).tag).toBe(`p`) }) - + test('component v-if', () => { const { node } = parseWithIfTransform(``) expect(node.type).toBe(NodeTypes.IF) diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 432492aa2..ba2587fdd 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -48,6 +48,10 @@ export { trackVForSlotScopes, trackSlotScopes } from './transforms/vSlot' -export { transformElement, resolveComponentType, buildProps } from './transforms/transformElement' +export { + transformElement, + resolveComponentType, + buildProps +} from './transforms/transformElement' export { processSlotOutlet } from './transforms/transformSlotOutlet' export { generateCodeFrame } from '@vue/shared' diff --git a/packages/runtime-dom/jsx.d.ts b/packages/runtime-dom/jsx.d.ts index cdf6f409d..8748c4f66 100644 --- a/packages/runtime-dom/jsx.d.ts +++ b/packages/runtime-dom/jsx.d.ts @@ -79,7 +79,15 @@ interface AriaAttributes { */ 'aria-controls'?: string /** Indicates the element that represents the current item within a container or set of related elements. */ - 'aria-current'?: boolean | 'false' | 'true' | 'page' | 'step' | 'location' | 'date' | 'time' + 'aria-current'?: + | boolean + | 'false' + | 'true' + | 'page' + | 'step' + | 'location' + | 'date' + | 'time' /** * Identifies the element (or elements) that describes the object. * @see aria-labelledby @@ -118,7 +126,15 @@ interface AriaAttributes { */ 'aria-grabbed'?: boolean | 'false' | 'true' /** Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. */ - 'aria-haspopup'?: boolean | 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' + 'aria-haspopup'?: + | boolean + | 'false' + | 'true' + | 'menu' + | 'listbox' + | 'tree' + | 'grid' + | 'dialog' /** * Indicates whether the element is exposed to an accessibility API. * @see aria-disabled. @@ -228,16 +244,15 @@ interface AriaAttributes { 'aria-valuetext'?: string } -export interface HTMLAttributes extends AriaAttributes{ - - domPropsInnerHTML?: string; +export interface HTMLAttributes extends AriaAttributes { + domPropsInnerHTML?: string class?: any style?: string | CSSProperties // Standard HTML Attributes accesskey?: string - contenteditable?: Booleanish | "inherit" + contenteditable?: Booleanish | 'inherit' contextmenu?: string dir?: string draggable?: Booleanish @@ -285,7 +300,15 @@ export interface HTMLAttributes extends AriaAttributes{ * Hints at the type of data that might be entered by the user while editing the element or its contents * @see https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-inputmode-attribute */ - inputmode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search' + inputmode?: + | 'none' + | 'text' + | 'tel' + | 'url' + | 'email' + | 'numeric' + | 'decimal' + | 'search' /** * Specify that a standard HTML element should behave like a defined custom built-in element * @see https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is @@ -317,8 +340,7 @@ export interface AreaHTMLAttributes extends HTMLAttributes { target?: string } -export interface AudioHTMLAttributes extends MediaHTMLAttributes { -} +export interface AudioHTMLAttributes extends MediaHTMLAttributes {} export interface BaseHTMLAttributes extends HTMLAttributes { href?: string @@ -712,8 +734,7 @@ export interface WebViewHTMLAttributes extends HTMLAttributes { } export interface SVGAttributes extends AriaAttributes { - - domPropsInnerHTML?: string; + domPropsInnerHTML?: string color?: string height?: number | string @@ -736,7 +757,20 @@ export interface SVGAttributes extends AriaAttributes { 'accent-height'?: number | string accumulate?: 'none' | 'sum' additive?: 'replace' | 'sum' - 'alignment-baseline'?: 'auto' | 'baseline' | 'before-edge' | 'text-before-edge' | 'middle' | 'central' | 'after-edge' | 'text-after-edge' | 'ideographic' | 'alphabetic' | 'hanging' | 'mathematical' | 'inherit' + 'alignment-baseline'?: + | 'auto' + | 'baseline' + | 'before-edge' + | 'text-before-edge' + | 'middle' + | 'central' + | 'after-edge' + | 'text-after-edge' + | 'ideographic' + | 'alphabetic' + | 'hanging' + | 'mathematical' + | 'inherit' allowReorder?: 'no' | 'yes' alphabetic?: number | string amplitude?: number | string @@ -955,13 +989,13 @@ export interface SVGAttributes extends AriaAttributes { x?: number | string xChannelSelector?: string 'x-height'?: number | string - 'xlinkActuate'?: string - 'xlinkArcrole'?: string - 'xlinkHref'?: string - 'xlinkRole'?: string - 'xlinkShow'?: string - 'xlinkTitle'?: string - 'xlinkType'?: string + xlinkActuate?: string + xlinkArcrole?: string + xlinkHref?: string + xlinkRole?: string + xlinkShow?: string + xlinkTitle?: string + xlinkType?: string y1?: number | string y2?: number | string y?: number | string diff --git a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts index d17c0dd11..958e47080 100644 --- a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts +++ b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts @@ -53,9 +53,7 @@ export function ssrRenderDynamicAttr( if (isBooleanAttr(attrKey)) { return value === false ? `` : ` ${attrKey}` } else if (isSSRSafeAttrName(attrKey)) { - return value === '' - ? ` ${attrKey}` - : ` ${attrKey}="${escapeHtml(value)}"` + return value === '' ? ` ${attrKey}` : ` ${attrKey}="${escapeHtml(value)}"` } else { console.warn( `[@vue/server-renderer] Skipped rendering unsafe attribute name: ${attrKey}`