wip: setup context + emit
This commit is contained in:
parent
08806073a1
commit
5228f0343b
@ -68,12 +68,11 @@ test('type inference w/ optional props declaration', () => {
|
|||||||
a: 1
|
a: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
render(props) {
|
render(ctx) {
|
||||||
props.msg
|
ctx.msg
|
||||||
|
ctx.a * 2
|
||||||
|
this.msg
|
||||||
this.a * 2
|
this.a * 2
|
||||||
// should not make state and this indexable
|
|
||||||
// state.foobar
|
|
||||||
// this.foobar
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
;(<Comp msg="hello"/>)
|
;(<Comp msg="hello"/>)
|
||||||
@ -96,9 +95,10 @@ test('type inference w/ array props declaration', () => {
|
|||||||
c: 1
|
c: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
render(props) {
|
render(ctx) {
|
||||||
props.a
|
ctx.a
|
||||||
props.b
|
ctx.b
|
||||||
|
ctx.c
|
||||||
this.a
|
this.a
|
||||||
this.b
|
this.b
|
||||||
this.c
|
this.c
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
state,
|
state,
|
||||||
immutableState
|
immutableState
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import { EMPTY_OBJ, isFunction } from '@vue/shared'
|
import { EMPTY_OBJ, isFunction, capitalize, invokeHandlers } from '@vue/shared'
|
||||||
import { RenderProxyHandlers } from './componentProxy'
|
import { RenderProxyHandlers } from './componentProxy'
|
||||||
import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
|
import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
|
||||||
import { PROPS, DYNAMIC_SLOTS, FULL_PROPS } from './patchFlags'
|
import { PROPS, DYNAMIC_SLOTS, FULL_PROPS } from './patchFlags'
|
||||||
@ -24,33 +24,26 @@ export type ComponentRenderProxy<P = {}, S = {}, PublicProps = P> = {
|
|||||||
$slots: Data
|
$slots: Data
|
||||||
$root: ComponentInstance | null
|
$root: ComponentInstance | null
|
||||||
$parent: ComponentInstance | null
|
$parent: ComponentInstance | null
|
||||||
|
$emit: (event: string, ...args: any[]) => void
|
||||||
} & P &
|
} & P &
|
||||||
S
|
S
|
||||||
|
|
||||||
type RenderFunction<P = Data> = (
|
type SetupFunction<Props, RawBindings> = (
|
||||||
props: P,
|
props: Props,
|
||||||
slots: Slots,
|
ctx: SetupContext
|
||||||
attrs: Data,
|
) => RawBindings | (() => VNodeChild)
|
||||||
vnode: VNode
|
|
||||||
) => any
|
|
||||||
|
|
||||||
type RenderFunctionWithThis<Props, RawBindings> = <
|
type RenderFunction<Props = {}, RawBindings = {}> = <
|
||||||
Bindings extends UnwrapValue<RawBindings>
|
Bindings extends UnwrapValue<RawBindings>
|
||||||
>(
|
>(
|
||||||
this: ComponentRenderProxy<Props, Bindings>,
|
this: ComponentRenderProxy<Props, Bindings>,
|
||||||
props: Props,
|
ctx: ComponentRenderProxy<Props, Bindings>
|
||||||
slots: Slots,
|
|
||||||
attrs: Data,
|
|
||||||
vnode: VNode
|
|
||||||
) => VNodeChild
|
) => VNodeChild
|
||||||
|
|
||||||
interface ComponentOptionsWithoutProps<Props = Data, RawBindings = Data> {
|
interface ComponentOptionsWithoutProps<Props = Data, RawBindings = Data> {
|
||||||
props?: undefined
|
props?: undefined
|
||||||
setup?: (
|
setup?: SetupFunction<Props, RawBindings>
|
||||||
this: ComponentRenderProxy<Props>,
|
render?: RenderFunction<Props, RawBindings>
|
||||||
props: Props
|
|
||||||
) => RawBindings | RenderFunction<Props>
|
|
||||||
render?: RenderFunctionWithThis<Props, RawBindings>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentOptionsWithArrayProps<
|
interface ComponentOptionsWithArrayProps<
|
||||||
@ -59,11 +52,8 @@ interface ComponentOptionsWithArrayProps<
|
|||||||
Props = { [key in PropNames]?: any }
|
Props = { [key in PropNames]?: any }
|
||||||
> {
|
> {
|
||||||
props: PropNames[]
|
props: PropNames[]
|
||||||
setup?: (
|
setup?: SetupFunction<Props, RawBindings>
|
||||||
this: ComponentRenderProxy<Props>,
|
render?: RenderFunction<Props, RawBindings>
|
||||||
props: Props
|
|
||||||
) => RawBindings | RenderFunction<Props>
|
|
||||||
render?: RenderFunctionWithThis<Props, RawBindings>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentOptionsWithProps<
|
interface ComponentOptionsWithProps<
|
||||||
@ -72,11 +62,8 @@ interface ComponentOptionsWithProps<
|
|||||||
Props = ExtractPropTypes<PropsOptions>
|
Props = ExtractPropTypes<PropsOptions>
|
||||||
> {
|
> {
|
||||||
props: PropsOptions
|
props: PropsOptions
|
||||||
setup?: (
|
setup?: SetupFunction<Props, RawBindings>
|
||||||
this: ComponentRenderProxy<Props>,
|
render?: RenderFunction<Props, RawBindings>
|
||||||
props: Props
|
|
||||||
) => RawBindings | RenderFunction<Props>
|
|
||||||
render?: RenderFunctionWithThis<Props, RawBindings>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComponentOptions =
|
export type ComponentOptions =
|
||||||
@ -84,14 +71,15 @@ export type ComponentOptions =
|
|||||||
| ComponentOptionsWithoutProps
|
| ComponentOptionsWithoutProps
|
||||||
| ComponentOptionsWithArrayProps
|
| ComponentOptionsWithArrayProps
|
||||||
|
|
||||||
export interface FunctionalComponent<P = {}> extends RenderFunction<P> {
|
export interface FunctionalComponent<P = {}> {
|
||||||
|
(props: P, ctx: SetupContext): VNodeChild
|
||||||
props?: ComponentPropsOptions<P>
|
props?: ComponentPropsOptions<P>
|
||||||
displayName?: string
|
displayName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LifecycleHook = Function[] | null
|
type LifecycleHook = Function[] | null
|
||||||
|
|
||||||
export interface LifecycleHooks {
|
interface LifecycleHooks {
|
||||||
bm: LifecycleHook // beforeMount
|
bm: LifecycleHook // beforeMount
|
||||||
m: LifecycleHook // mounted
|
m: LifecycleHook // mounted
|
||||||
bu: LifecycleHook // beforeUpdate
|
bu: LifecycleHook // beforeUpdate
|
||||||
@ -105,6 +93,13 @@ export interface LifecycleHooks {
|
|||||||
ec: LifecycleHook // errorCaptured
|
ec: LifecycleHook // errorCaptured
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SetupContext {
|
||||||
|
attrs: Data
|
||||||
|
slots: Slots
|
||||||
|
refs: Data
|
||||||
|
emit: ((event: string, ...args: any[]) => void)
|
||||||
|
}
|
||||||
|
|
||||||
export type ComponentInstance<P = Data, S = Data> = {
|
export type ComponentInstance<P = Data, S = Data> = {
|
||||||
type: FunctionalComponent | ComponentOptions
|
type: FunctionalComponent | ComponentOptions
|
||||||
parent: ComponentInstance | null
|
parent: ComponentInstance | null
|
||||||
@ -114,22 +109,22 @@ export type ComponentInstance<P = Data, S = Data> = {
|
|||||||
subTree: VNode
|
subTree: VNode
|
||||||
update: ReactiveEffect
|
update: ReactiveEffect
|
||||||
effects: ReactiveEffect[] | null
|
effects: ReactiveEffect[] | null
|
||||||
render: RenderFunction<P> | null
|
render: RenderFunction<P, S> | null
|
||||||
|
|
||||||
// the rest are only for stateful components
|
// the rest are only for stateful components
|
||||||
renderProxy: ComponentRenderProxy | null
|
|
||||||
propsProxy: Data | null
|
|
||||||
state: S
|
state: S
|
||||||
props: P
|
props: P
|
||||||
attrs: Data
|
renderProxy: ComponentRenderProxy | null
|
||||||
slots: Slots
|
propsProxy: P | null
|
||||||
refs: Data
|
setupContext: SetupContext | null
|
||||||
} & LifecycleHooks
|
} & SetupContext &
|
||||||
|
LifecycleHooks
|
||||||
|
|
||||||
// createComponent
|
// createComponent
|
||||||
// overload 1: direct setup function
|
// overload 1: direct setup function
|
||||||
// (uses user defined props interface)
|
// (uses user defined props interface)
|
||||||
export function createComponent<Props>(
|
export function createComponent<Props>(
|
||||||
setup: (props: Props) => RenderFunction<Props>
|
setup: (props: Props, ctx: SetupContext) => (() => any)
|
||||||
): (props: Props) => any
|
): (props: Props) => any
|
||||||
// overload 2: object format with no props
|
// overload 2: object format with no props
|
||||||
// (uses user defined props interface)
|
// (uses user defined props interface)
|
||||||
@ -182,6 +177,7 @@ export function createComponentInstance(
|
|||||||
render: null,
|
render: null,
|
||||||
renderProxy: null,
|
renderProxy: null,
|
||||||
propsProxy: null,
|
propsProxy: null,
|
||||||
|
setupContext: null,
|
||||||
|
|
||||||
bm: null,
|
bm: null,
|
||||||
m: null,
|
m: null,
|
||||||
@ -201,7 +197,15 @@ export function createComponentInstance(
|
|||||||
props: EMPTY_OBJ,
|
props: EMPTY_OBJ,
|
||||||
attrs: EMPTY_OBJ,
|
attrs: EMPTY_OBJ,
|
||||||
slots: 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
|
instance.root = parent ? parent.root : instance
|
||||||
@ -223,12 +227,13 @@ export function setupStatefulComponent(instance: ComponentInstance) {
|
|||||||
currentInstance = instance
|
currentInstance = instance
|
||||||
// the props proxy makes the props object passed to setup() reactive
|
// the props proxy makes the props object passed to setup() reactive
|
||||||
// so props change can be tracked by watchers
|
// 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
|
// it will be updated in resolveProps() on updates before render
|
||||||
const propsProxy = (instance.propsProxy = setup.length
|
const propsProxy = (instance.propsProxy = setup.length
|
||||||
? immutableState(instance.props)
|
? immutableState(instance.props)
|
||||||
: null)
|
: 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)) {
|
if (isFunction(setupResult)) {
|
||||||
// setup returned an inline render function
|
// setup returned an inline render function
|
||||||
instance.render = setupResult
|
instance.render = setupResult
|
||||||
@ -245,22 +250,55 @@ export function setupStatefulComponent(instance: ComponentInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
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) {
|
if (vnode.shapeFlag & STATEFUL_COMPONENT) {
|
||||||
return normalizeVNode(
|
return normalizeVNode(
|
||||||
(instance.render as RenderFunction).call(
|
(instance.render as RenderFunction).call(renderProxy, props, setupContext)
|
||||||
renderProxy,
|
|
||||||
props,
|
|
||||||
slots,
|
|
||||||
attrs,
|
|
||||||
vnode
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// functional
|
// functional
|
||||||
return normalizeVNode(
|
return normalizeVNode(
|
||||||
(Component as FunctionalComponent)(props, slots, attrs, vnode)
|
(Component as FunctionalComponent)(props, {
|
||||||
|
attrs,
|
||||||
|
slots,
|
||||||
|
refs,
|
||||||
|
emit
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ export const RenderProxyHandlers = {
|
|||||||
return target.root
|
return target.root
|
||||||
case '$el':
|
case '$el':
|
||||||
return target.vnode && target.vnode.el
|
return target.vnode && target.vnode.el
|
||||||
|
case '$emit':
|
||||||
|
return target.emit
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { invokeHandlers } from '@vue/shared'
|
||||||
|
|
||||||
interface Invoker extends Function {
|
interface Invoker extends Function {
|
||||||
value: EventValue
|
value: EventValue
|
||||||
lastUpdated?: number
|
lastUpdated?: number
|
||||||
@ -56,30 +58,18 @@ export function patchEvent(
|
|||||||
|
|
||||||
function createInvoker(value: any) {
|
function createInvoker(value: any) {
|
||||||
const invoker = ((e: Event) => {
|
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
|
}) as any
|
||||||
invoker.value = value
|
invoker.value = value
|
||||||
value.invoker = invoker
|
value.invoker = invoker
|
||||||
invoker.lastUpdated = getNow()
|
invoker.lastUpdated = getNow()
|
||||||
return invoker
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -30,3 +30,16 @@ export const hyphenate = (str: string): string => {
|
|||||||
export const capitalize = (str: string): string => {
|
export const capitalize = (str: string): string => {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user