diff --git a/packages/runtime-core/__tests__/createComponent.spec.tsx b/packages/runtime-core/__tests__/createComponent.spec.tsx index 885b58de..7c81aadf 100644 --- a/packages/runtime-core/__tests__/createComponent.spec.tsx +++ b/packages/runtime-core/__tests__/createComponent.spec.tsx @@ -68,12 +68,11 @@ test('type inference w/ optional props declaration', () => { a: 1 } }, - render(props) { - props.msg + render(ctx) { + ctx.msg + ctx.a * 2 + this.msg this.a * 2 - // should not make state and this indexable - // state.foobar - // this.foobar } }) ;() @@ -96,9 +95,10 @@ test('type inference w/ array props declaration', () => { c: 1 } }, - render(props) { - props.a - props.b + render(ctx) { + ctx.a + ctx.b + ctx.c this.a this.b this.c diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index a92951c3..17e5cd81 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -5,7 +5,7 @@ import { state, immutableState } from '@vue/reactivity' -import { EMPTY_OBJ, isFunction } from '@vue/shared' +import { EMPTY_OBJ, isFunction, capitalize, invokeHandlers } from '@vue/shared' import { RenderProxyHandlers } from './componentProxy' import { ComponentPropsOptions, ExtractPropTypes } from './componentProps' import { PROPS, DYNAMIC_SLOTS, FULL_PROPS } from './patchFlags' @@ -24,33 +24,26 @@ export type ComponentRenderProxy

= { $slots: Data $root: ComponentInstance | null $parent: ComponentInstance | null + $emit: (event: string, ...args: any[]) => void } & P & S -type RenderFunction

= ( - props: P, - slots: Slots, - attrs: Data, - vnode: VNode -) => any +type SetupFunction = ( + props: Props, + ctx: SetupContext +) => RawBindings | (() => VNodeChild) -type RenderFunctionWithThis = < +type RenderFunction = < Bindings extends UnwrapValue >( this: ComponentRenderProxy, - props: Props, - slots: Slots, - attrs: Data, - vnode: VNode + ctx: ComponentRenderProxy ) => VNodeChild interface ComponentOptionsWithoutProps { props?: undefined - setup?: ( - this: ComponentRenderProxy, - props: Props - ) => RawBindings | RenderFunction - render?: RenderFunctionWithThis + setup?: SetupFunction + render?: RenderFunction } interface ComponentOptionsWithArrayProps< @@ -59,11 +52,8 @@ interface ComponentOptionsWithArrayProps< Props = { [key in PropNames]?: any } > { props: PropNames[] - setup?: ( - this: ComponentRenderProxy, - props: Props - ) => RawBindings | RenderFunction - render?: RenderFunctionWithThis + setup?: SetupFunction + render?: RenderFunction } interface ComponentOptionsWithProps< @@ -72,11 +62,8 @@ interface ComponentOptionsWithProps< Props = ExtractPropTypes > { props: PropsOptions - setup?: ( - this: ComponentRenderProxy, - props: Props - ) => RawBindings | RenderFunction - render?: RenderFunctionWithThis + setup?: SetupFunction + render?: RenderFunction } export type ComponentOptions = @@ -84,14 +71,15 @@ export type ComponentOptions = | ComponentOptionsWithoutProps | ComponentOptionsWithArrayProps -export interface FunctionalComponent

extends RenderFunction

{ +export interface FunctionalComponent

{ + (props: P, ctx: SetupContext): VNodeChild props?: ComponentPropsOptions

displayName?: string } type LifecycleHook = Function[] | null -export interface LifecycleHooks { +interface LifecycleHooks { bm: LifecycleHook // beforeMount m: LifecycleHook // mounted bu: LifecycleHook // beforeUpdate @@ -105,6 +93,13 @@ export interface LifecycleHooks { ec: LifecycleHook // errorCaptured } +interface SetupContext { + attrs: Data + slots: Slots + refs: Data + emit: ((event: string, ...args: any[]) => void) +} + export type ComponentInstance

= { type: FunctionalComponent | ComponentOptions parent: ComponentInstance | null @@ -114,22 +109,22 @@ export type ComponentInstance

= { subTree: VNode update: ReactiveEffect effects: ReactiveEffect[] | null - render: RenderFunction

| null + render: RenderFunction | null + // the rest are only for stateful components - renderProxy: ComponentRenderProxy | null - propsProxy: Data | null state: S props: P - attrs: Data - slots: Slots - refs: Data -} & LifecycleHooks + renderProxy: ComponentRenderProxy | null + propsProxy: P | null + setupContext: SetupContext | null +} & SetupContext & + LifecycleHooks // createComponent // overload 1: direct setup function // (uses user defined props interface) export function createComponent( - setup: (props: Props) => RenderFunction + setup: (props: Props, ctx: SetupContext) => (() => any) ): (props: Props) => any // overload 2: object format with no props // (uses user defined props interface) @@ -182,6 +177,7 @@ export function createComponentInstance( render: null, renderProxy: null, propsProxy: null, + setupContext: null, bm: null, m: null, @@ -201,7 +197,15 @@ export function createComponentInstance( props: EMPTY_OBJ, attrs: EMPTY_OBJ, slots: EMPTY_OBJ, - refs: EMPTY_OBJ + refs: EMPTY_OBJ, + + emit: (event: string, ...args: any[]) => { + const props = instance.vnode.props || EMPTY_OBJ + const handler = props[`on${event}`] || props[`on${capitalize(event)}`] + if (handler) { + invokeHandlers(handler, args) + } + } } instance.root = parent ? parent.root : instance @@ -223,12 +227,13 @@ export function setupStatefulComponent(instance: ComponentInstance) { currentInstance = instance // the props proxy makes the props object passed to setup() reactive // so props change can be tracked by watchers - // only need to create it if setup() actually expects it // it will be updated in resolveProps() on updates before render const propsProxy = (instance.propsProxy = setup.length ? immutableState(instance.props) : null) - const setupResult = setup.call(proxy, propsProxy) + const setupContext = (instance.setupContext = + setup.length > 1 ? createSetupContext(instance) : null) + const setupResult = setup.call(proxy, propsProxy, setupContext) if (isFunction(setupResult)) { // setup returned an inline render function instance.render = setupResult @@ -245,22 +250,55 @@ export function setupStatefulComponent(instance: ComponentInstance) { } } +const SetupProxyHandlers: { [key: string]: ProxyHandler } = {} +;['attrs', 'slots', 'refs'].forEach((type: string) => { + SetupProxyHandlers[type] = { + get: (instance: any, key: string) => (instance[type] as any)[key], + has: (instance: any, key: string) => key in (instance[type] as any), + ownKeys: (instance: any) => Object.keys(instance[type] as any), + set: () => false, + deleteProperty: () => false + } +}) + +function createSetupContext(instance: ComponentInstance): SetupContext { + const context = { + // attrs, slots & refs are non-reactive, but they need to always expose + // the latest values (instance.xxx may get replaced during updates) so we + // need to expose them through a proxy + attrs: new Proxy(instance, SetupProxyHandlers.attrs), + slots: new Proxy(instance, SetupProxyHandlers.slots), + refs: new Proxy(instance, SetupProxyHandlers.refs), + emit: instance.emit + } as any + return __DEV__ ? Object.freeze(context) : context +} + export function renderComponentRoot(instance: ComponentInstance): VNode { - const { type: Component, renderProxy, props, slots, attrs, vnode } = instance + const { + type: Component, + vnode, + renderProxy, + setupContext, + props, + slots, + attrs, + refs, + emit + } = instance if (vnode.shapeFlag & STATEFUL_COMPONENT) { return normalizeVNode( - (instance.render as RenderFunction).call( - renderProxy, - props, - slots, - attrs, - vnode - ) + (instance.render as RenderFunction).call(renderProxy, props, setupContext) ) } else { // functional return normalizeVNode( - (Component as FunctionalComponent)(props, slots, attrs, vnode) + (Component as FunctionalComponent)(props, { + attrs, + slots, + refs, + emit + }) ) } } diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 4d30a9be..90b75be1 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -25,6 +25,8 @@ export const RenderProxyHandlers = { return target.root case '$el': return target.vnode && target.vnode.el + case '$emit': + return target.emit default: break } diff --git a/packages/runtime-dom/src/modules/events.ts b/packages/runtime-dom/src/modules/events.ts index 92b5914e..60c9136e 100644 --- a/packages/runtime-dom/src/modules/events.ts +++ b/packages/runtime-dom/src/modules/events.ts @@ -1,3 +1,5 @@ +import { invokeHandlers } from '@vue/shared' + interface Invoker extends Function { value: EventValue lastUpdated?: number @@ -56,30 +58,18 @@ export function patchEvent( function createInvoker(value: any) { const invoker = ((e: Event) => { - invokeEvents(e, invoker.value, invoker.lastUpdated) + // async edge case #6566: inner click event triggers patch, event handler + // attached to outer element during patch, and triggered again. This + // happens because browsers fire microtask ticks between event propagation. + // the solution is simple: we save the timestamp when a handler is attached, + // and the handler would only fire if the event passed to it was fired + // AFTER it was attached. + if (e.timeStamp >= invoker.lastUpdated) { + invokeHandlers(invoker.value, [e]) + } }) as any invoker.value = value value.invoker = invoker invoker.lastUpdated = getNow() return invoker } - -function invokeEvents(e: Event, value: EventValue, lastUpdated: number) { - // async edge case #6566: inner click event triggers patch, event handler - // attached to outer element during patch, and triggered again. This - // happens because browsers fire microtask ticks between event propagation. - // the solution is simple: we save the timestamp when a handler is attached, - // and the handler would only fire if the event passed to it was fired - // AFTER it was attached. - if (e.timeStamp < lastUpdated) { - return - } - - if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - value[i](e) - } - } else { - value(e) - } -} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 1884ae88..3e670db3 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -30,3 +30,16 @@ export const hyphenate = (str: string): string => { export const capitalize = (str: string): string => { return str.charAt(0).toUpperCase() + str.slice(1) } + +export function invokeHandlers( + handlers: Function | Function[], + args: any[] = EMPTY_ARR +) { + if (isArray(handlers)) { + for (let i = 0; i < handlers.length; i++) { + handlers[i].apply(null, args) + } + } else { + handlers.apply(null, args) + } +}