From 55566e8f520eee8a07b85221174989c47c443c35 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 10 Apr 2020 10:59:46 -0400 Subject: [PATCH] refactor(runtime-core): remove emit return value BREAKING CHANGE: this.$emit() and setupContext.emit() no longer return values. For logic that relies on return value of listeners, the listener should be declared as an `onXXX` prop and be called directly. This still allows the parent component to pass in a handler using `v-on`, since `v-on:foo` internally compiles to `onFoo`. ref: https://github.com/vuejs/rfcs/pull/16 --- .../runtime-core/__tests__/component.spec.ts | 146 ------------------ .../__tests__/componentEmits.spec.ts | 30 +++- .../__tests__/componentSlots.spec.ts | 47 ++++++ .../__tests__/errorHandling.spec.ts | 25 +-- packages/runtime-core/src/componentEmits.ts | 13 +- 5 files changed, 95 insertions(+), 166 deletions(-) delete mode 100644 packages/runtime-core/__tests__/component.spec.ts create mode 100644 packages/runtime-core/__tests__/componentSlots.spec.ts diff --git a/packages/runtime-core/__tests__/component.spec.ts b/packages/runtime-core/__tests__/component.spec.ts deleted file mode 100644 index dad6f8ca8bb..00000000000 --- a/packages/runtime-core/__tests__/component.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { - h, - ref, - render, - nodeOps, - nextTick, - defineComponent -} from '@vue/runtime-test' - -describe('renderer: component', () => { - test.todo('should work') - - test.todo('shouldUpdateComponent') - - test.todo('componentProxy') - - describe('componentProps', () => { - test.todo('should work') - - test('should convert empty booleans to true', () => { - let b1: any, b2: any, b3: any - - const Comp = defineComponent({ - props: { - b1: Boolean, - b2: [Boolean, String], - b3: [String, Boolean] - }, - setup(props) { - ;({ b1, b2, b3 } = props) - return () => '' - } - }) - - render( - h(Comp, { b1: '', b2: '', b3: '' }), - nodeOps.createElement('div') - ) - - expect(b1).toBe(true) - expect(b2).toBe(true) - expect(b3).toBe('') - }) - }) - - describe('slots', () => { - test('should respect $stable flag', async () => { - const flag1 = ref(1) - const flag2 = ref(2) - const spy = jest.fn() - - const Child = () => { - spy() - return 'child' - } - - const App = { - setup() { - return () => [ - flag1.value, - h( - Child, - { n: flag2.value }, - { - foo: () => 'foo', - $stable: true - } - ) - ] - } - } - - render(h(App), nodeOps.createElement('div')) - expect(spy).toHaveBeenCalledTimes(1) - - // parent re-render, props didn't change, slots are stable - // -> child should not update - flag1.value++ - await nextTick() - expect(spy).toHaveBeenCalledTimes(1) - - // parent re-render, props changed - // -> child should update - flag2.value++ - await nextTick() - expect(spy).toHaveBeenCalledTimes(2) - }) - }) - - test('emit', async () => { - let noMatchEmitResult: any - let singleEmitResult: any - let multiEmitResult: any - - const Child = defineComponent({ - setup(_, { emit }) { - noMatchEmitResult = emit('foo') - singleEmitResult = emit('bar') - multiEmitResult = emit('baz') - return () => h('div') - } - }) - - const App = { - setup() { - return () => - h(Child, { - // emit triggering single handler - onBar: () => 1, - // emit triggering multiple handlers - onBaz: [() => Promise.resolve(2), () => Promise.resolve(3)] - }) - } - } - - render(h(App), nodeOps.createElement('div')) - - // assert return values from emit - expect(noMatchEmitResult).toMatchObject([]) - expect(singleEmitResult).toMatchObject([1]) - expect(await Promise.all(multiEmitResult)).toMatchObject([2, 3]) - }) - - // for v-model:foo-bar usage in DOM templates - test('emit update:xxx events should trigger kebab-case equivalent', () => { - const Child = defineComponent({ - setup(_, { emit }) { - emit('update:fooBar', 1) - return () => h('div') - } - }) - - const handler = jest.fn() - const App = { - setup() { - return () => - h(Child, { - 'onUpdate:foo-bar': handler - }) - } - } - - render(h(App), nodeOps.createElement('div')) - expect(handler).toHaveBeenCalled() - }) -}) diff --git a/packages/runtime-core/__tests__/componentEmits.spec.ts b/packages/runtime-core/__tests__/componentEmits.spec.ts index 331b814cd3c..2807366bcfe 100644 --- a/packages/runtime-core/__tests__/componentEmits.spec.ts +++ b/packages/runtime-core/__tests__/componentEmits.spec.ts @@ -5,7 +5,7 @@ import { mockWarn } from '@vue/shared' import { render, defineComponent, h, nodeOps } from '@vue/runtime-test' import { isEmitListener } from '../src/componentEmits' -describe('emits option', () => { +describe('component: emit', () => { mockWarn() test('trigger both raw event and capitalize handlers', () => { @@ -27,6 +27,7 @@ describe('emits option', () => { expect(onBar).toHaveBeenCalled() }) + // for v-model:foo-bar usage in DOM templates test('trigger hyphendated events for update:xxx events', () => { const Foo = defineComponent({ render() {}, @@ -49,6 +50,33 @@ describe('emits option', () => { expect(barSpy).toHaveBeenCalled() }) + test('should trigger array of listeners', async () => { + const Child = defineComponent({ + setup(_, { emit }) { + emit('foo', 1) + return () => h('div') + } + }) + + const fn1 = jest.fn() + const fn2 = jest.fn() + + const App = { + setup() { + return () => + h(Child, { + onFoo: [fn1, fn2] + }) + } + } + + render(h(App), nodeOps.createElement('div')) + expect(fn1).toHaveBeenCalledTimes(1) + expect(fn1).toHaveBeenCalledWith(1) + expect(fn2).toHaveBeenCalledTimes(1) + expect(fn1).toHaveBeenCalledWith(1) + }) + test('warning for undeclared event (array)', () => { const Foo = defineComponent({ emits: ['foo'], diff --git a/packages/runtime-core/__tests__/componentSlots.spec.ts b/packages/runtime-core/__tests__/componentSlots.spec.ts new file mode 100644 index 00000000000..ee856820cb6 --- /dev/null +++ b/packages/runtime-core/__tests__/componentSlots.spec.ts @@ -0,0 +1,47 @@ +import { ref, render, h, nodeOps, nextTick } from '@vue/runtime-test' + +describe('component: slots', () => { + // TODO more tests for slots normalization etc. + + test('should respect $stable flag', async () => { + const flag1 = ref(1) + const flag2 = ref(2) + const spy = jest.fn() + + const Child = () => { + spy() + return 'child' + } + + const App = { + setup() { + return () => [ + flag1.value, + h( + Child, + { n: flag2.value }, + { + foo: () => 'foo', + $stable: true + } + ) + ] + } + } + + render(h(App), nodeOps.createElement('div')) + expect(spy).toHaveBeenCalledTimes(1) + + // parent re-render, props didn't change, slots are stable + // -> child should not update + flag1.value++ + await nextTick() + expect(spy).toHaveBeenCalledTimes(1) + + // parent re-render, props changed + // -> child should update + flag2.value++ + await nextTick() + expect(spy).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/runtime-core/__tests__/errorHandling.spec.ts b/packages/runtime-core/__tests__/errorHandling.spec.ts index f6907949b1f..4830b65eff5 100644 --- a/packages/runtime-core/__tests__/errorHandling.spec.ts +++ b/packages/runtime-core/__tests__/errorHandling.spec.ts @@ -416,21 +416,16 @@ describe('error handling', () => { } } - let res: any const Child = { + props: ['onFoo'], setup(props: any, { emit }: any) { - res = emit('foo') + emit('foo') return () => null } } render(h(Comp), nodeOps.createElement('div')) - - try { - await Promise.all(res) - } catch (e) { - expect(e).toBe(err) - } + await nextTick() expect(fn).toHaveBeenCalledWith(err, 'component event handler') }) @@ -438,6 +433,12 @@ describe('error handling', () => { const err = new Error('foo') const fn = jest.fn() + const res: Promise[] = [] + const createAsyncHandler = (p: Promise) => () => { + res.push(p) + return p + } + const Comp = { setup() { onErrorCaptured((err, instance, info) => { @@ -446,15 +447,17 @@ describe('error handling', () => { }) return () => h(Child, { - onFoo: [() => Promise.reject(err), () => Promise.resolve(1)] + onFoo: [ + createAsyncHandler(Promise.reject(err)), + createAsyncHandler(Promise.resolve(1)) + ] }) } } - let res: any const Child = { setup(props: any, { emit }: any) { - res = emit('foo') + emit('foo') return () => null } } diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index 7ece305c04f..0cd348dd5ce 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -28,12 +28,12 @@ export type EmitFn< Options = ObjectEmitsOptions, Event extends keyof Options = keyof Options > = Options extends any[] - ? (event: Options[0], ...args: any[]) => unknown[] + ? (event: Options[0], ...args: any[]) => void : UnionToIntersection< { [key in Event]: Options[key] extends ((...args: infer Args) => any) - ? (event: key, ...args: Args) => unknown[] - : (event: key, ...args: any[]) => unknown[] + ? (event: key, ...args: Args) => void + : (event: key, ...args: any[]) => void }[Event] > @@ -41,7 +41,7 @@ export function emit( instance: ComponentInternalInstance, event: string, ...args: any[] -): any[] { +) { const props = instance.vnode.props || EMPTY_OBJ if (__DEV__) { @@ -74,15 +74,12 @@ export function emit( handler = props[`on${event}`] || props[`on${capitalize(event)}`] } if (handler) { - const res = callWithAsyncErrorHandling( + callWithAsyncErrorHandling( handler, instance, ErrorCodes.COMPONENT_EVENT_HANDLER, args ) - return isArray(res) ? res : [res] - } else { - return [] } }