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
// 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
proxy?: any
/**
@ -538,9 +555,6 @@ export interface ComponentInternalInstance extends GenericComponentInstance {
asyncResolved: boolean
// lifecycle
isMounted: boolean
isUnmounted: boolean
isDeactivated: boolean
/**
* @internal
*/

View File

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

View File

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

View File

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

View File

@ -57,7 +57,7 @@ interface SharedInternalOptions {
/**
* Cached normalized props proxy handlers.
*/
__propsHandlers?: [ProxyHandler<any>, ProxyHandler<any>]
__propsHandlers?: [ProxyHandler<any> | null, ProxyHandler<any>]
/**
* Cached normalized emits options.
*/
@ -124,6 +124,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
block: Block
scope: EffectScope
rawProps: RawProps | undefined
props: Record<string, any>
attrs: Record<string, any>
exposed?: Record<string, any>
@ -138,29 +139,38 @@ export class VaporComponentInstance implements GenericComponentInstance {
hasFallthrough: boolean
isMounted: boolean
isUnmounted: boolean
isDeactivated: boolean
// LifecycleHooks.ERROR_CAPTURED
ec: LifecycleHook
// dev only
propsOptions?: NormalizedPropsOptions
emitsOptions?: ObjectEmitsOptions | null
constructor(comp: VaporComponent, rawProps?: RawProps) {
this.uid = nextUid()
this.type = comp
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.scope = new EffectScope(true)
this.rawProps = rawProps
this.provides = this.refs = EMPTY_OBJ
this.emitted = null
this.ec = null
this.emitted = this.ec = null
this.isMounted = this.isUnmounted = this.isDeactivated = false
// init props
this.propsDefaults = null
this.hasFallthrough = false
if (comp.props && rawProps && rawProps.$) {
if (rawProps && rawProps.$) {
// has dynamic props, use proxy
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.hasFallthrough = true
} 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 {
type VaporComponent,
type VaporComponentInstance,
currentInstance,
} 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
@ -43,5 +48,19 @@ export function emit(
event: string,
...rawArgs: any[]
): 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 [propsOptions, needCastKeys] = normalizePropsOptions(comp)
const emitsOptions = normalizeEmitsOptions(comp)
// for dev emit check
if (__DEV__) {
instance.propsOptions = normalizePropsOptions(comp)
instance.emitsOptions = emitsOptions
}
for (const key in rawProps) {
const normalizedKey = camelize(key)
const needCast = needCastKeys && needCastKeys.includes(normalizedKey)
@ -91,21 +98,23 @@ function resolveDefault(
}
// 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
}
const passThrough = (val: any) => val
export function getDynamicPropsHandlers(
comp: VaporComponent,
instance: VaporComponentInstance,
): [ProxyHandler<RawProps>, ProxyHandler<RawProps>] {
): [ProxyHandler<RawProps> | null, ProxyHandler<RawProps>] {
if (comp.__propsHandlers) {
return comp.__propsHandlers
}
let normalizedKeys: string[] | undefined
const propsOptions = normalizePropsOptions(comp)[0]!
const propsOptions = normalizePropsOptions(comp)[0]
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) => {
if (key === '$') return
@ -114,7 +123,8 @@ export function getDynamicPropsHandlers(
} else if (isProp(key) || isEmitListener(emitsOptions, key)) {
return
}
const castProp = (value: any, isAbsent = false) =>
const castProp = propsOptions
? (value: any, isAbsent = false) =>
asProp
? resolvePropValue(
propsOptions,
@ -125,6 +135,7 @@ export function getDynamicPropsHandlers(
isAbsent,
)
: value
: passThrough
if (key in target) {
return castProp(resolveSource(target[key as string]))
@ -142,7 +153,8 @@ export function getDynamicPropsHandlers(
return castProp(undefined, true)
}
const propsHandlers = {
const propsHandlers = propsOptions
? ({
get: (target, key: string) => getProp(target, key, true),
has: (_, key: string) => isProp(key),
getOwnPropertyDescriptor(target, key: string) {
@ -158,12 +170,12 @@ export function getDynamicPropsHandlers(
normalizedKeys || (normalizedKeys = Object.keys(propsOptions)),
set: NO,
deleteProperty: NO,
} satisfies ProxyHandler<RawProps>
} satisfies ProxyHandler<RawProps>)
: null
const hasAttr = (target: RawProps, key: string) => {
if (key === '$' || isProp(key) || isEmitListener(emitsOptions, key))
return false
if (hasOwn(target, key)) return true
if (target.$) {
let i = target.$.length
while (i--) {
@ -172,7 +184,7 @@ export function getDynamicPropsHandlers(
}
}
}
return false
return hasOwn(target, key)
}
const attrsHandlers = {
@ -188,14 +200,14 @@ export function getDynamicPropsHandlers(
}
},
ownKeys(target) {
const staticKeys = Object.keys(target)
const keys = Object.keys(target)
if (target.$) {
let i = target.$.length
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,
deleteProperty: NO,

View File

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