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)
+ }
+}