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'
$slots: Data
$root: ComponentInstance | null
$parent: ComponentInstance | null
+ $emit: (event: string, ...args: any[]) => void
} & P &
S
-type RenderFunction<P = Data> = (
- props: P,
- slots: Slots,
- attrs: Data,
- vnode: VNode
-) => any
+type SetupFunction<Props, RawBindings> = (
+ props: Props,
+ ctx: SetupContext
+) => RawBindings | (() => VNodeChild)
-type RenderFunctionWithThis<Props, RawBindings> = <
+type RenderFunction<Props = {}, RawBindings = {}> = <
Bindings extends UnwrapValue<RawBindings>
>(
this: ComponentRenderProxy<Props, Bindings>,
- props: Props,
- slots: Slots,
- attrs: Data,
- vnode: VNode
+ ctx: ComponentRenderProxy<Props, Bindings>
) => VNodeChild
interface ComponentOptionsWithoutProps<Props = Data, RawBindings = Data> {
props?: undefined
- setup?: (
- this: ComponentRenderProxy<Props>,
- props: Props
- ) => RawBindings | RenderFunction<Props>
- render?: RenderFunctionWithThis<Props, RawBindings>
+ setup?: SetupFunction<Props, RawBindings>
+ render?: RenderFunction<Props, RawBindings>
}
interface ComponentOptionsWithArrayProps<
Props = { [key in PropNames]?: any }
> {
props: PropNames[]
- setup?: (
- this: ComponentRenderProxy<Props>,
- props: Props
- ) => RawBindings | RenderFunction<Props>
- render?: RenderFunctionWithThis<Props, RawBindings>
+ setup?: SetupFunction<Props, RawBindings>
+ render?: RenderFunction<Props, RawBindings>
}
interface ComponentOptionsWithProps<
Props = ExtractPropTypes<PropsOptions>
> {
props: PropsOptions
- setup?: (
- this: ComponentRenderProxy<Props>,
- props: Props
- ) => RawBindings | RenderFunction<Props>
- render?: RenderFunctionWithThis<Props, RawBindings>
+ setup?: SetupFunction<Props, RawBindings>
+ render?: RenderFunction<Props, RawBindings>
}
export type ComponentOptions =
| ComponentOptionsWithoutProps
| ComponentOptionsWithArrayProps
-export interface FunctionalComponent<P = {}> extends RenderFunction<P> {
+export interface FunctionalComponent<P = {}> {
+ (props: P, ctx: SetupContext): VNodeChild
props?: ComponentPropsOptions<P>
displayName?: string
}
type LifecycleHook = Function[] | null
-export interface LifecycleHooks {
+interface LifecycleHooks {
bm: LifecycleHook // beforeMount
m: LifecycleHook // mounted
bu: LifecycleHook // beforeUpdate
ec: LifecycleHook // errorCaptured
}
+interface SetupContext {
+ attrs: Data
+ slots: Slots
+ refs: Data
+ emit: ((event: string, ...args: any[]) => void)
+}
+
export type ComponentInstance<P = Data, S = Data> = {
type: FunctionalComponent | ComponentOptions
parent: ComponentInstance | null
subTree: VNode
update: ReactiveEffect
effects: ReactiveEffect[] | null
- render: RenderFunction<P> | null
+ render: RenderFunction<P, S> | 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<Props>(
- setup: (props: Props) => RenderFunction<Props>
+ setup: (props: Props, ctx: SetupContext) => (() => any)
): (props: Props) => any
// overload 2: object format with no props
// (uses user defined props interface)
render: null,
renderProxy: null,
propsProxy: null,
+ setupContext: null,
bm: null,
m: null,
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
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
}
}
+const SetupProxyHandlers: { [key: string]: ProxyHandler<any> } = {}
+;['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
+ })
)
}
}
+import { invokeHandlers } from '@vue/shared'
+
interface Invoker extends Function {
value: EventValue
lastUpdated?: number
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)
- }
-}