wip: setup context + emit

This commit is contained in:
Evan You 2019-06-19 16:43:34 +08:00
parent 08806073a1
commit 5228f0343b
5 changed files with 121 additions and 78 deletions

View File

@ -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

View File

@ -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 {
if (vnode.shapeFlag & STATEFUL_COMPONENT) { type: Component,
return normalizeVNode( vnode,
(instance.render as RenderFunction).call(
renderProxy, renderProxy,
setupContext,
props, props,
slots, slots,
attrs, attrs,
vnode refs,
) emit
} = instance
if (vnode.shapeFlag & STATEFUL_COMPONENT) {
return normalizeVNode(
(instance.render as RenderFunction).call(renderProxy, props, setupContext)
) )
} else { } else {
// functional // functional
return normalizeVNode( return normalizeVNode(
(Component as FunctionalComponent)(props, slots, attrs, vnode) (Component as FunctionalComponent)(props, {
attrs,
slots,
refs,
emit
})
) )
} }
} }

View File

@ -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
} }

View File

@ -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)
}) 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 // async edge case #6566: inner click event triggers patch, event handler
// attached to outer element during patch, and triggered again. This // attached to outer element during patch, and triggered again. This
// happens because browsers fire microtask ticks between event propagation. // happens because browsers fire microtask ticks between event propagation.
// the solution is simple: we save the timestamp when a handler is attached, // 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 // and the handler would only fire if the event passed to it was fired
// AFTER it was attached. // AFTER it was attached.
if (e.timeStamp < lastUpdated) { if (e.timeStamp >= invoker.lastUpdated) {
return invokeHandlers(invoker.value, [e])
}
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
value[i](e)
}
} else {
value(e)
} }
}) as any
invoker.value = value
value.invoker = invoker
invoker.lastUpdated = getNow()
return invoker
} }

View File

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