wip: emits

This commit is contained in:
Evan You 2024-12-03 22:49:28 +08:00
parent 72d82353ee
commit 65fc9769f2
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
8 changed files with 157 additions and 71 deletions

View File

@ -366,6 +366,23 @@ export interface GenericComponentInstance {
*/ */
propsDefaults: Data | null propsDefaults: Data | null
// lifecycle
isMounted: boolean
isUnmounted: boolean
isDeactivated: boolean
// for vapor the following two are dev only
/**
* resolved props options
* @internal
*/
propsOptions?: NormalizedPropsOptions
/**
* resolved emits options
* @internal
*/
emitsOptions?: ObjectEmitsOptions | null
// the following are for error handling logic only // the following are for error handling logic only
proxy?: any proxy?: any
/** /**
@ -538,9 +555,6 @@ export interface ComponentInternalInstance extends GenericComponentInstance {
asyncResolved: boolean asyncResolved: boolean
// lifecycle // lifecycle
isMounted: boolean
isUnmounted: boolean
isDeactivated: boolean
/** /**
* @internal * @internal
*/ */

View File

@ -18,6 +18,7 @@ import {
type ComponentInternalInstance, type ComponentInternalInstance,
type ComponentOptions, type ComponentOptions,
type ConcreteComponent, type ConcreteComponent,
type GenericComponentInstance,
formatComponentName, formatComponentName,
} from './component' } from './component'
import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling' import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
@ -114,13 +115,27 @@ export function emit(
...rawArgs: any[] ...rawArgs: any[]
): ComponentPublicInstance | null | undefined { ): ComponentPublicInstance | null | undefined {
if (instance.isUnmounted) return if (instance.isUnmounted) return
const props = instance.vnode.props || EMPTY_OBJ return baseEmit(
instance,
instance.vnode.props || EMPTY_OBJ,
defaultPropGetter,
event,
...rawArgs,
)
}
/**
* @internal for vapor only
*/
export function baseEmit(
instance: GenericComponentInstance,
props: Record<string, any>,
getter: (props: Record<string, any>, key: string) => unknown,
event: string,
...rawArgs: any[]
): ComponentPublicInstance | null | undefined {
if (__DEV__) { if (__DEV__) {
const { const { emitsOptions, propsOptions } = instance
emitsOptions,
propsOptions: [propsOptions],
} = instance
if (emitsOptions) { if (emitsOptions) {
if ( if (
!(event in emitsOptions) && !(event in emitsOptions) &&
@ -130,7 +145,11 @@ export function emit(
event.startsWith(compatModelEventPrefix)) event.startsWith(compatModelEventPrefix))
) )
) { ) {
if (!propsOptions || !(toHandlerKey(camelize(event)) in propsOptions)) { if (
!propsOptions ||
!propsOptions[0] ||
!(toHandlerKey(camelize(event)) in propsOptions[0])
) {
warn( warn(
`Component emitted event "${event}" but it is neither declared in ` + `Component emitted event "${event}" but it is neither declared in ` +
`the emits option nor as an "${toHandlerKey(camelize(event))}" prop.`, `the emits option nor as an "${toHandlerKey(camelize(event))}" prop.`,
@ -170,7 +189,10 @@ export function emit(
if (__DEV__) { if (__DEV__) {
const lowerCaseEvent = event.toLowerCase() const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && props[toHandlerKey(lowerCaseEvent)]) { if (
lowerCaseEvent !== event &&
getter(props, toHandlerKey(lowerCaseEvent))
) {
warn( warn(
`Event "${lowerCaseEvent}" is emitted in component ` + `Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName( `${formatComponentName(
@ -188,18 +210,18 @@ export function emit(
let handlerName let handlerName
let handler = let handler =
props[(handlerName = toHandlerKey(event))] || getter(props, (handlerName = toHandlerKey(event))) ||
// also try camelCase event handler (#2249) // also try camelCase event handler (#2249)
props[(handlerName = toHandlerKey(camelize(event)))] getter(props, (handlerName = toHandlerKey(camelize(event))))
// for v-model update:xxx events, also trigger kebab-case equivalent // for v-model update:xxx events, also trigger kebab-case equivalent
// for props passed via kebab-case // for props passed via kebab-case
if (!handler && isModelListener) { if (!handler && isModelListener) {
handler = props[(handlerName = toHandlerKey(hyphenate(event)))] handler = getter(props, (handlerName = toHandlerKey(hyphenate(event))))
} }
if (handler) { if (handler) {
callWithAsyncErrorHandling( callWithAsyncErrorHandling(
handler, handler as Function,
instance, instance,
ErrorCodes.COMPONENT_EVENT_HANDLER, ErrorCodes.COMPONENT_EVENT_HANDLER,
args, args,
@ -222,12 +244,20 @@ export function emit(
) )
} }
if (__COMPAT__) { if (__COMPAT__ && args) {
compatModelEmit(instance, event, args) compatModelEmit(instance as ComponentInternalInstance, event, args)
return compatInstanceEmit(instance, event, args) return compatInstanceEmit(
instance as ComponentInternalInstance,
event,
args,
)
} }
} }
function defaultPropGetter(props: Record<string, any>, key: string): unknown {
return props[key]
}
export function normalizeEmitsOptions( export function normalizeEmitsOptions(
comp: ConcreteComponent, comp: ConcreteComponent,
appContext: AppContext, appContext: AppContext,

View File

@ -1,7 +1,7 @@
/* eslint-disable no-restricted-globals */ /* eslint-disable no-restricted-globals */
import type { App } from './apiCreateApp' import type { App } from './apiCreateApp'
import { Comment, Fragment, Static, Text } from './vnode' import { Comment, Fragment, Static, Text } from './vnode'
import type { ComponentInternalInstance } from './component' import type { GenericComponentInstance } from './component'
interface AppRecord { interface AppRecord {
id: number id: number
@ -111,7 +111,7 @@ const _devtoolsComponentRemoved = /*@__PURE__*/ createDevtoolsComponentHook(
) )
export const devtoolsComponentRemoved = ( export const devtoolsComponentRemoved = (
component: ComponentInternalInstance, component: GenericComponentInstance,
): void => { ): void => {
if ( if (
devtools && devtools &&
@ -123,13 +123,13 @@ export const devtoolsComponentRemoved = (
} }
} }
type DevtoolsComponentHook = (component: ComponentInternalInstance) => void type DevtoolsComponentHook = (component: GenericComponentInstance) => void
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
function createDevtoolsComponentHook( function createDevtoolsComponentHook(
hook: DevtoolsHooks, hook: DevtoolsHooks,
): DevtoolsComponentHook { ): DevtoolsComponentHook {
return (component: ComponentInternalInstance) => { return (component: GenericComponentInstance) => {
emit( emit(
hook, hook,
component.appContext.app, component.appContext.app,
@ -147,20 +147,20 @@ export const devtoolsPerfEnd: DevtoolsPerformanceHook =
/*@__PURE__*/ createDevtoolsPerformanceHook(DevtoolsHooks.PERFORMANCE_END) /*@__PURE__*/ createDevtoolsPerformanceHook(DevtoolsHooks.PERFORMANCE_END)
type DevtoolsPerformanceHook = ( type DevtoolsPerformanceHook = (
component: ComponentInternalInstance, component: GenericComponentInstance,
type: string, type: string,
time: number, time: number,
) => void ) => void
function createDevtoolsPerformanceHook( function createDevtoolsPerformanceHook(
hook: DevtoolsHooks, hook: DevtoolsHooks,
): DevtoolsPerformanceHook { ): DevtoolsPerformanceHook {
return (component: ComponentInternalInstance, type: string, time: number) => { return (component: GenericComponentInstance, type: string, time: number) => {
emit(hook, component.appContext.app, component.uid, component, type, time) emit(hook, component.appContext.app, component.uid, component, type, time)
} }
} }
export function devtoolsComponentEmit( export function devtoolsComponentEmit(
component: ComponentInternalInstance, component: GenericComponentInstance,
event: string, event: string,
params: any[], params: any[],
): void { ): void {

View File

@ -491,7 +491,7 @@ export {
baseNormalizePropsOptions, baseNormalizePropsOptions,
resolvePropValue, resolvePropValue,
} from './componentProps' } from './componentProps'
export { isEmitListener } from './componentEmits' export { baseEmit, isEmitListener } from './componentEmits'
export { type SchedulerJob, queueJob } from './scheduler' export { type SchedulerJob, queueJob } from './scheduler'
export { export {
type ComponentInternalOptions, type ComponentInternalOptions,

View File

@ -57,7 +57,7 @@ interface SharedInternalOptions {
/** /**
* Cached normalized props proxy handlers. * Cached normalized props proxy handlers.
*/ */
__propsHandlers?: [ProxyHandler<any>, ProxyHandler<any>] __propsHandlers?: [ProxyHandler<any> | null, ProxyHandler<any>]
/** /**
* Cached normalized emits options. * Cached normalized emits options.
*/ */
@ -124,6 +124,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
block: Block block: Block
scope: EffectScope scope: EffectScope
rawProps: RawProps | undefined
props: Record<string, any> props: Record<string, any>
attrs: Record<string, any> attrs: Record<string, any>
exposed?: Record<string, any> exposed?: Record<string, any>
@ -138,29 +139,38 @@ export class VaporComponentInstance implements GenericComponentInstance {
hasFallthrough: boolean hasFallthrough: boolean
isMounted: boolean
isUnmounted: boolean
isDeactivated: boolean
// LifecycleHooks.ERROR_CAPTURED // LifecycleHooks.ERROR_CAPTURED
ec: LifecycleHook ec: LifecycleHook
// dev only
propsOptions?: NormalizedPropsOptions
emitsOptions?: ObjectEmitsOptions | null
constructor(comp: VaporComponent, rawProps?: RawProps) { constructor(comp: VaporComponent, rawProps?: RawProps) {
this.uid = nextUid() this.uid = nextUid()
this.type = comp this.type = comp
this.parent = currentInstance this.parent = currentInstance
this.appContext = currentInstance ? currentInstance.appContext : null! // TODO // @ts-expect-error TODO use proper appContext
this.appContext = currentInstance ? currentInstance.appContext : {}
this.block = null! // to be set this.block = null! // to be set
this.scope = new EffectScope(true) this.scope = new EffectScope(true)
this.rawProps = rawProps
this.provides = this.refs = EMPTY_OBJ this.provides = this.refs = EMPTY_OBJ
this.emitted = null this.emitted = this.ec = null
this.ec = null this.isMounted = this.isUnmounted = this.isDeactivated = false
// init props // init props
this.propsDefaults = null this.propsDefaults = null
this.hasFallthrough = false this.hasFallthrough = false
if (comp.props && rawProps && rawProps.$) { if (rawProps && rawProps.$) {
// has dynamic props, use proxy // has dynamic props, use proxy
const handlers = getDynamicPropsHandlers(comp, this) const handlers = getDynamicPropsHandlers(comp, this)
this.props = new Proxy(rawProps, handlers[0]) this.props = comp.props ? new Proxy(rawProps, handlers[0]!) : EMPTY_OBJ
this.attrs = new Proxy(rawProps, handlers[1]) this.attrs = new Proxy(rawProps, handlers[1])
this.hasFallthrough = true this.hasFallthrough = true
} else { } else {

View File

@ -1,10 +1,15 @@
import type { EmitFn, ObjectEmitsOptions } from '@vue/runtime-core' import {
type EmitFn,
type ObjectEmitsOptions,
baseEmit,
} from '@vue/runtime-core'
import { import {
type VaporComponent, type VaporComponent,
type VaporComponentInstance, type VaporComponentInstance,
currentInstance, currentInstance,
} from './component' } from './component'
import { NOOP, isArray } from '@vue/shared' import { NOOP, hasOwn, isArray } from '@vue/shared'
import { resolveSource } from './componentProps'
/** /**
* The logic from core isn't too reusable so it's better to duplicate here * The logic from core isn't too reusable so it's better to duplicate here
@ -43,5 +48,19 @@ export function emit(
event: string, event: string,
...rawArgs: any[] ...rawArgs: any[]
): void { ): void {
// TODO extract reusable logic from core const rawProps = instance.rawProps
if (!rawProps || instance.isUnmounted) return
baseEmit(instance, rawProps, propGetter, event, ...rawArgs)
}
function propGetter(rawProps: Record<string, any>, key: string) {
const dynamicSources = rawProps.$
if (dynamicSources) {
let i = dynamicSources.length
while (i--) {
const source = resolveSource(dynamicSources[i])
if (hasOwn(source, key)) return source[key]
}
}
return rawProps[key] && rawProps[key]()
} }

View File

@ -26,6 +26,13 @@ export function initStaticProps(
const { props, attrs } = instance const { props, attrs } = instance
const [propsOptions, needCastKeys] = normalizePropsOptions(comp) const [propsOptions, needCastKeys] = normalizePropsOptions(comp)
const emitsOptions = normalizeEmitsOptions(comp) const emitsOptions = normalizeEmitsOptions(comp)
// for dev emit check
if (__DEV__) {
instance.propsOptions = normalizePropsOptions(comp)
instance.emitsOptions = emitsOptions
}
for (const key in rawProps) { for (const key in rawProps) {
const normalizedKey = camelize(key) const normalizedKey = camelize(key)
const needCast = needCastKeys && needCastKeys.includes(normalizedKey) const needCast = needCastKeys && needCastKeys.includes(normalizedKey)
@ -91,21 +98,23 @@ function resolveDefault(
} }
// TODO optimization: maybe convert functions into computeds // TODO optimization: maybe convert functions into computeds
function resolveSource(source: PropSource): Record<string, any> { export function resolveSource(source: PropSource): Record<string, any> {
return isFunction(source) ? source() : source return isFunction(source) ? source() : source
} }
const passThrough = (val: any) => val
export function getDynamicPropsHandlers( export function getDynamicPropsHandlers(
comp: VaporComponent, comp: VaporComponent,
instance: VaporComponentInstance, instance: VaporComponentInstance,
): [ProxyHandler<RawProps>, ProxyHandler<RawProps>] { ): [ProxyHandler<RawProps> | null, ProxyHandler<RawProps>] {
if (comp.__propsHandlers) { if (comp.__propsHandlers) {
return comp.__propsHandlers return comp.__propsHandlers
} }
let normalizedKeys: string[] | undefined let normalizedKeys: string[] | undefined
const propsOptions = normalizePropsOptions(comp)[0]! const propsOptions = normalizePropsOptions(comp)[0]
const emitsOptions = normalizeEmitsOptions(comp) const emitsOptions = normalizeEmitsOptions(comp)
const isProp = (key: string) => hasOwn(propsOptions, key) const isProp = propsOptions ? (key: string) => hasOwn(propsOptions, key) : NO
const getProp = (target: RawProps, key: string, asProp: boolean) => { const getProp = (target: RawProps, key: string, asProp: boolean) => {
if (key === '$') return if (key === '$') return
@ -114,7 +123,8 @@ export function getDynamicPropsHandlers(
} else if (isProp(key) || isEmitListener(emitsOptions, key)) { } else if (isProp(key) || isEmitListener(emitsOptions, key)) {
return return
} }
const castProp = (value: any, isAbsent = false) => const castProp = propsOptions
? (value: any, isAbsent = false) =>
asProp asProp
? resolvePropValue( ? resolvePropValue(
propsOptions, propsOptions,
@ -125,6 +135,7 @@ export function getDynamicPropsHandlers(
isAbsent, isAbsent,
) )
: value : value
: passThrough
if (key in target) { if (key in target) {
return castProp(resolveSource(target[key as string])) return castProp(resolveSource(target[key as string]))
@ -142,7 +153,8 @@ export function getDynamicPropsHandlers(
return castProp(undefined, true) return castProp(undefined, true)
} }
const propsHandlers = { const propsHandlers = propsOptions
? ({
get: (target, key: string) => getProp(target, key, true), get: (target, key: string) => getProp(target, key, true),
has: (_, key: string) => isProp(key), has: (_, key: string) => isProp(key),
getOwnPropertyDescriptor(target, key: string) { getOwnPropertyDescriptor(target, key: string) {
@ -158,12 +170,12 @@ export function getDynamicPropsHandlers(
normalizedKeys || (normalizedKeys = Object.keys(propsOptions)), normalizedKeys || (normalizedKeys = Object.keys(propsOptions)),
set: NO, set: NO,
deleteProperty: NO, deleteProperty: NO,
} satisfies ProxyHandler<RawProps> } satisfies ProxyHandler<RawProps>)
: null
const hasAttr = (target: RawProps, key: string) => { const hasAttr = (target: RawProps, key: string) => {
if (key === '$' || isProp(key) || isEmitListener(emitsOptions, key)) if (key === '$' || isProp(key) || isEmitListener(emitsOptions, key))
return false return false
if (hasOwn(target, key)) return true
if (target.$) { if (target.$) {
let i = target.$.length let i = target.$.length
while (i--) { while (i--) {
@ -172,7 +184,7 @@ export function getDynamicPropsHandlers(
} }
} }
} }
return false return hasOwn(target, key)
} }
const attrsHandlers = { const attrsHandlers = {
@ -188,14 +200,14 @@ export function getDynamicPropsHandlers(
} }
}, },
ownKeys(target) { ownKeys(target) {
const staticKeys = Object.keys(target) const keys = Object.keys(target)
if (target.$) { if (target.$) {
let i = target.$.length let i = target.$.length
while (i--) { while (i--) {
staticKeys.push(...Object.keys(resolveSource(target.$[i]))) keys.push(...Object.keys(resolveSource(target.$[i])))
} }
} }
return staticKeys.filter(key => hasAttr(target, key)) return keys.filter(key => hasAttr(target, key))
}, },
set: NO, set: NO,
deleteProperty: NO, deleteProperty: NO,

View File

@ -1,3 +1,4 @@
export { createComponent as createComponentSimple } from './component' export { createComponent as createComponentSimple } from './component'
export { renderEffect as renderEffectSimple } from './renderEffect' export { renderEffect as renderEffectSimple } from './renderEffect'
export { createVaporApp as createVaporAppSimple } from './apiCreateApp' export { createVaporApp as createVaporAppSimple } from './apiCreateApp'
export { useEmit } from './componentEmits'