refactor: make core warning and errorHandling vdom/vapor generic

This commit is contained in:
Evan You 2024-12-03 21:43:18 +08:00
parent 4ea66770be
commit 72d82353ee
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
11 changed files with 260 additions and 178 deletions

View File

@ -235,6 +235,17 @@ export interface ClassComponent {
__vccOpts: ComponentOptions
}
/**
* Type used where a function accepts both vdom and vapor components.
*/
export type GenericComponent = (
| {
name?: string
}
| ((() => any) & { displayName?: string })
) &
ComponentInternalOptions
/**
* Concrete component type matches its actual value: it's either an options
* object, or a function. Use this where the code expects to work with actual
@ -308,11 +319,66 @@ export type InternalRenderFunction = {
_compatWrapped?: boolean // is wrapped for v2 compat
}
/**
* Base component instance interface that is shared between vdom mode and vapor
* mode, so that we can have a mixed instance tree and reuse core logic that
* operate on both.
*/
export interface GenericComponentInstance {
uid: number
type: GenericComponent
parent: GenericComponentInstance | null
appContext: AppContext
/**
* Object containing values this component provides for its descendants
* @internal
*/
provides: Data
/**
* Tracking reactive effects (e.g. watchers) associated with this component
* so that they can be automatically stopped on component unmount
* @internal
*/
scope: EffectScope
/**
* SSR render function
* (they are the same between vdom and vapor components.)
* @internal
*/
ssrRender?: Function | null
// state
props: Data
attrs: Data
/**
* @internal
*/
refs: Data
/**
* used for keeping track of .once event handlers on components
* @internal
*/
emitted: Record<string, boolean> | null
/**
* used for caching the value returned from props default factory functions to
* avoid unnecessary watcher trigger
* @internal
*/
propsDefaults: Data | null
// the following are for error handling logic only
proxy?: any
/**
* @internal
*/
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
}
/**
* We expose a subset of properties on the internal instance as they are
* useful for advanced external libraries and tools.
*/
export interface ComponentInternalInstance {
export interface ComponentInternalInstance extends GenericComponentInstance {
uid: number
type: ConcreteComponent
parent: ComponentInternalInstance | null
@ -348,16 +414,6 @@ export interface ComponentInternalInstance {
* @internal
*/
render: InternalRenderFunction | null
/**
* SSR render function
* @internal
*/
ssrRender?: Function | null
/**
* Object containing values this component provides for its descendants
* @internal
*/
provides: Data
/**
* for tracking useId()
* first element is the current boundary prefix
@ -365,12 +421,6 @@ export interface ComponentInternalInstance {
* @internal
*/
ids: [string, number, number]
/**
* Tracking reactive effects (e.g. watchers) associated with this component
* so that they can be automatically stopped on component unmount
* @internal
*/
scope: EffectScope
/**
* cache for proxy access type to avoid hasOwnProperty calls
* @internal
@ -430,10 +480,27 @@ export interface ComponentInternalInstance {
ceReload?: (newStyles?: string[]) => void
// the rest are only for stateful components ---------------------------------
/**
* @internal
*/
setupContext: SetupContext | null
/**
* setup related
* @internal
*/
setupState: Data | null
/**
* devtools access to additional info
* @internal
*/
devtoolsRawSetupState?: any
// main proxy that serves as the public instance (`this`)
proxy: ComponentPublicInstance | null
data: Data // options API only
emit: EmitFn
slots: InternalSlots
// exposed properties via expose()
exposed: Record<string, any> | null
exposeProxy: Record<string, any> | null
@ -451,41 +518,6 @@ export interface ComponentInternalInstance {
* @internal
*/
ctx: Data
// state
data: Data
props: Data
attrs: Data
slots: InternalSlots
refs: Data
emit: EmitFn
/**
* used for keeping track of .once event handlers on components
* @internal
*/
emitted: Record<string, boolean> | null
/**
* used for caching the value returned from props default factory functions to
* avoid unnecessary watcher trigger
* @internal
*/
propsDefaults: Data | null
/**
* setup related
* @internal
*/
setupState: Data
/**
* devtools access to additional info
* @internal
*/
devtoolsRawSetupState?: any
/**
* @internal
*/
setupContext: SetupContext | null
/**
* suspense related
* @internal
@ -600,6 +632,13 @@ const emptyAppContext = createAppContext()
let uid = 0
/**
* @internal for vapor
*/
export function nextUid(): number {
return uid++
}
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
@ -1202,7 +1241,7 @@ const classify = (str: string): string =>
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
export function getComponentName(
Component: ConcreteComponent,
Component: GenericComponent,
includeInferred = true,
): string | false | undefined {
return isFunction(Component)
@ -1211,8 +1250,8 @@ export function getComponentName(
}
export function formatComponentName(
instance: ComponentInternalInstance | null,
Component: ConcreteComponent,
instance: GenericComponentInstance | null,
Component: GenericComponent,
isRoot = false,
): string {
let name = getComponentName(Component)
@ -1234,7 +1273,7 @@ export function formatComponentName(
}
name =
inferFromRegistry(
instance.components ||
(instance as ComponentInternalInstance).components ||
(instance.parent.type as ComponentOptions).components,
) || inferFromRegistry(instance.appContext.components)
}

View File

@ -30,6 +30,7 @@ import {
type ComponentInternalInstance,
type ComponentOptions,
type ConcreteComponent,
type GenericComponentInstance,
setCurrentInstance,
} from './component'
import { isEmitListener } from './componentEmits'
@ -183,9 +184,15 @@ type NormalizedProp = PropOptions & {
[BooleanFlags.shouldCastTrue]?: boolean
}
// normalized value is a tuple of the actual normalized options
// and an array of prop keys that need value casting (booleans and defaults)
/**
* normalized value is a tuple of the actual normalized options
* and an array of prop keys that need value casting (booleans and defaults)
* @internal
*/
export type NormalizedProps = Record<string, NormalizedProp>
/**
* @internal
*/
export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
export function initProps(
@ -444,18 +451,12 @@ function setFullProps(
return hasAttrsChanged
}
/**
* A type that allows both vdom and vapor instances
*/
type CommonInstance = Pick<
ComponentInternalInstance,
'props' | 'propsDefaults' | 'ce'
>
/**
* @internal for runtime-vapor
*/
export function resolvePropValue<T extends CommonInstance>(
export function resolvePropValue<
T extends GenericComponentInstance & Pick<ComponentInternalInstance, 'ce'>,
>(
options: NormalizedProps,
key: string,
value: unknown,

View File

@ -1,6 +1,5 @@
import { pauseTracking, resetTracking } from '@vue/reactivity'
import type { VNode } from './vnode'
import type { ComponentInternalInstance } from './component'
import type { GenericComponentInstance } from './component'
import { popWarningContext, pushWarningContext, warn } from './warning'
import { EMPTY_OBJ, isArray, isFunction, isPromise } from '@vue/shared'
import { LifecycleHooks } from './enums'
@ -69,7 +68,7 @@ export type ErrorTypes = LifecycleHooks | ErrorCodes | WatchErrorCodes
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null | undefined,
instance: GenericComponentInstance | null | undefined,
type: ErrorTypes,
args?: unknown[],
): any {
@ -82,7 +81,7 @@ export function callWithErrorHandling(
export function callWithAsyncErrorHandling(
fn: Function | Function[],
instance: ComponentInternalInstance | null,
instance: GenericComponentInstance | null,
type: ErrorTypes,
args?: unknown[],
): any {
@ -111,17 +110,16 @@ export function callWithAsyncErrorHandling(
export function handleError(
err: unknown,
instance: ComponentInternalInstance | null | undefined,
instance: GenericComponentInstance | null | undefined,
type: ErrorTypes,
throwInDev = true,
): void {
const contextVNode = instance ? instance.vnode : null
const { errorHandler, throwUnhandledErrorInProduction } =
(instance && instance.appContext.config) || EMPTY_OBJ
if (instance) {
let cur = instance.parent
// the exposed instance is the render proxy to keep it consistent with 2.x
const exposedInstance = instance.proxy
const exposedInstance = instance.proxy || instance
// in production the hook receives only the error code
const errorInfo = __DEV__
? ErrorTypeStrings[type]
@ -151,23 +149,23 @@ export function handleError(
return
}
}
logError(err, type, contextVNode, throwInDev, throwUnhandledErrorInProduction)
logError(err, type, instance, throwInDev, throwUnhandledErrorInProduction)
}
function logError(
err: unknown,
type: ErrorTypes,
contextVNode: VNode | null,
instance: GenericComponentInstance | null | undefined,
throwInDev = true,
throwInProd = false,
) {
if (__DEV__) {
const info = ErrorTypeStrings[type]
if (contextVNode) {
pushWarningContext(contextVNode)
if (instance) {
pushWarningContext(instance)
}
warn(`Unhandled error${info ? ` during execution of ${info}` : ``}`)
if (contextVNode) {
if (instance) {
popWarningContext()
}
// crash in dev by default so it's more noticeable

View File

@ -320,7 +320,6 @@ export type {
ExtractPropTypes,
ExtractPublicPropTypes,
ExtractDefaultPropTypes,
NormalizedPropsOptions,
} from './componentProps'
export type {
Directive,
@ -487,6 +486,16 @@ export const DeprecationTypes = (
// **IMPORTANT** These APIs are exposed solely for @vue/runtime-vapor and may
// change without notice between versions. User code should never rely on them.
export { baseNormalizePropsOptions, resolvePropValue } from './componentProps'
export {
type NormalizedPropsOptions,
baseNormalizePropsOptions,
resolvePropValue,
} from './componentProps'
export { isEmitListener } from './componentEmits'
export { type SchedulerJob, queueJob } from './scheduler'
export {
type ComponentInternalOptions,
type GenericComponentInstance,
type LifecycleHook,
nextUid,
} from './component'

View File

@ -1,6 +1,6 @@
import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
import { NOOP, isArray } from '@vue/shared'
import { type ComponentInternalInstance, getComponentName } from './component'
import { type GenericComponentInstance, getComponentName } from './component'
export enum SchedulerJobFlags {
QUEUED = 1 << 0,
@ -38,7 +38,7 @@ export interface SchedulerJob extends Function {
* Attached by renderer.ts when setting up a component's render effect
* Used to obtain component information when reporting max recursive updates.
*/
i?: ComponentInternalInstance
i?: GenericComponentInstance
}
export type SchedulerJobs = SchedulerJob | SchedulerJob[]
@ -141,7 +141,7 @@ export function queuePostFlushCb(cb: SchedulerJobs): void {
}
export function flushPreFlushCbs(
instance?: ComponentInternalInstance,
instance?: GenericComponentInstance,
seen?: CountMap,
// skip the current job
i: number = flushIndex + 1,

View File

@ -1,29 +1,27 @@
import type { VNode } from './vnode'
import {
type ComponentInternalInstance,
type ConcreteComponent,
type GenericComponentInstance,
formatComponentName,
} from './component'
import { isFunction, isString } from '@vue/shared'
import type { Data } from '@vue/runtime-shared'
import { isRef, pauseTracking, resetTracking, toRaw } from '@vue/reactivity'
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import { type VNode, isVNode } from './vnode'
type ComponentVNode = VNode & {
type: ConcreteComponent
}
const stack: VNode[] = []
const stack: (GenericComponentInstance | VNode)[] = []
type TraceEntry = {
vnode: ComponentVNode
ctx: GenericComponentInstance | VNode
recurseCount: number
}
type ComponentTraceStack = TraceEntry[]
export function pushWarningContext(vnode: VNode): void {
stack.push(vnode)
export function pushWarningContext(
ctx: GenericComponentInstance | VNode,
): void {
stack.push(ctx)
}
export function popWarningContext(): void {
@ -40,7 +38,8 @@ export function warn(msg: string, ...args: any[]): void {
// during patch, leading to infinite recursion.
pauseTracking()
const instance = stack.length ? stack[stack.length - 1].component : null
const entry = stack.length ? stack[stack.length - 1] : null
const instance = isVNode(entry) ? entry.component : entry
const appWarnHandler = instance && instance.appContext.config.warnHandler
const trace = getComponentTrace()
@ -55,7 +54,8 @@ export function warn(msg: string, ...args: any[]): void {
instance && instance.proxy,
trace
.map(
({ vnode }) => `at <${formatComponentName(instance, vnode.type)}>`,
({ ctx }) =>
`at <${formatComponentName(instance, (ctx as any).type)}>`,
)
.join('\n'),
trace,
@ -79,8 +79,8 @@ export function warn(msg: string, ...args: any[]): void {
}
export function getComponentTrace(): ComponentTraceStack {
let currentVNode: VNode | null = stack[stack.length - 1]
if (!currentVNode) {
let currentCtx: TraceEntry['ctx'] | null = stack[stack.length - 1]
if (!currentCtx) {
return []
}
@ -89,19 +89,23 @@ export function getComponentTrace(): ComponentTraceStack {
// instance parent pointers.
const normalizedStack: ComponentTraceStack = []
while (currentVNode) {
while (currentCtx) {
const last = normalizedStack[0]
if (last && last.vnode === currentVNode) {
if (last && last.ctx === currentCtx) {
last.recurseCount++
} else {
normalizedStack.push({
vnode: currentVNode as ComponentVNode,
ctx: currentCtx,
recurseCount: 0,
})
}
const parentInstance: ComponentInternalInstance | null =
currentVNode.component && currentVNode.component.parent
currentVNode = parentInstance && parentInstance.vnode
if (isVNode(currentCtx)) {
const parent: ComponentInternalInstance | null =
currentCtx.component && currentCtx.component.parent
currentCtx = parent && parent.vnode
} else {
currentCtx = currentCtx.parent
}
}
return normalizedStack
@ -116,19 +120,14 @@ function formatTrace(trace: ComponentTraceStack): any[] {
return logs
}
function formatTraceEntry({ vnode, recurseCount }: TraceEntry): any[] {
function formatTraceEntry({ ctx, recurseCount }: TraceEntry): any[] {
const postfix =
recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``
const isRoot = vnode.component ? vnode.component.parent == null : false
const open = ` at <${formatComponentName(
vnode.component,
vnode.type,
isRoot,
)}`
const instance = isVNode(ctx) ? ctx.component : ctx
const isRoot = instance ? instance.parent == null : false
const open = ` at <${formatComponentName(instance, (ctx as any).type, isRoot)}`
const close = `>` + postfix
return vnode.props
? [open, ...formatProps(vnode.props), close]
: [open + close]
return ctx.props ? [open, ...formatProps(ctx.props), close] : [open + close]
}
function formatProps(props: Data): any[] {

View File

@ -1,8 +1,8 @@
import { normalizeContainer } from '../apiRender'
import { insert } from '../dom/element'
import { type Component, createComponent } from './component'
import { type VaporComponent, createComponent } from './component'
export function createVaporApp(comp: Component): any {
export function createVaporApp(comp: VaporComponent): any {
return {
mount(container: string | ParentNode) {
container = normalizeContainer(container)

View File

@ -1,14 +1,19 @@
import {
type AppContext,
type ComponentInternalOptions,
type ComponentPropsOptions,
EffectScope,
type EmitsOptions,
type GenericComponentInstance,
type LifecycleHook,
type NormalizedPropsOptions,
type ObjectEmitsOptions,
nextUid,
} from '@vue/runtime-core'
import type { Block } from '../block'
import type { Data } from '@vue/runtime-shared'
import { pauseTracking, resetTracking } from '@vue/reactivity'
import { isFunction } from '@vue/shared'
import { EMPTY_OBJ, isFunction } from '@vue/shared'
import {
type RawProps,
getDynamicPropsHandlers,
@ -17,22 +22,22 @@ import {
import { setDynamicProp } from '../dom/prop'
import { renderEffect } from './renderEffect'
export type Component = FunctionalComponent | ObjectComponent
export type VaporComponent = FunctionalVaporComponent | ObjectVaporComponent
export type SetupFn = (
export type VaporSetupFn = (
props: any,
ctx: SetupContext,
) => Block | Data | undefined
export type FunctionalComponent = SetupFn &
Omit<ObjectComponent, 'setup'> & {
export type FunctionalVaporComponent = VaporSetupFn &
Omit<ObjectVaporComponent, 'setup'> & {
displayName?: string
} & SharedInternalOptions
export interface ObjectComponent
export interface ObjectVaporComponent
extends ComponentInternalOptions,
SharedInternalOptions {
setup?: SetupFn
setup?: VaporSetupFn
inheritAttrs?: boolean
props?: ComponentPropsOptions
emits?: EmitsOptions
@ -43,41 +48,27 @@ export interface ObjectComponent
}
interface SharedInternalOptions {
/**
* Cached normalized props options.
* In vapor mode there are no mixins so normalized options can be cached
* directly on the component
*/
__propsOptions?: NormalizedPropsOptions
/**
* Cached normalized props proxy handlers.
*/
__propsHandlers?: [ProxyHandler<any>, ProxyHandler<any>]
/**
* Cached normalized emits options.
*/
__emitsOptions?: ObjectEmitsOptions
}
// Note: can't mark this whole interface internal because some public interfaces
// extend it.
interface ComponentInternalOptions {
/**
* @internal
*/
__scopeId?: string
/**
* @internal
*/
__cssModules?: Data
/**
* @internal
*/
__hmrId?: string
/**
* This one should be exposed so that devtools can make use of it
*/
__file?: string
/**
* name inferred from filename
*/
__name?: string
}
export function createComponent(
component: Component,
component: VaporComponent,
rawProps?: RawProps,
isSingleRoot?: boolean,
): ComponentInstance {
): VaporComponentInstance {
// check if we are the single root of the parent
// if yes, inject parent attrs as dynamic props source
if (isSingleRoot && currentInstance && currentInstance.hasFallthrough) {
@ -88,7 +79,7 @@ export function createComponent(
}
}
const instance = new ComponentInstance(component, rawProps)
const instance = new VaporComponentInstance(component, rawProps)
pauseTracking()
let prevInstance = currentInstance
@ -123,23 +114,45 @@ export function createComponent(
return instance
}
let uid = 0
export let currentInstance: ComponentInstance | null = null
export let currentInstance: VaporComponentInstance | null = null
export class VaporComponentInstance implements GenericComponentInstance {
uid: number
type: VaporComponent
parent: GenericComponentInstance | null
appContext: AppContext
export class ComponentInstance {
type: Component
uid: number = uid++
scope: EffectScope = new EffectScope(true)
props: Record<string, any>
propsDefaults: Record<string, any> | null
attrs: Record<string, any>
block: Block
scope: EffectScope
props: Record<string, any>
attrs: Record<string, any>
exposed?: Record<string, any>
emitted: Record<string, boolean> | null
propsDefaults: Record<string, any> | null
// for useTemplateRef()
refs: Data
// for provide / inject
provides: Data
hasFallthrough: boolean
constructor(comp: Component, rawProps?: RawProps) {
// LifecycleHooks.ERROR_CAPTURED
ec: LifecycleHook
constructor(comp: VaporComponent, rawProps?: RawProps) {
this.uid = nextUid()
this.type = comp
this.parent = currentInstance
this.appContext = currentInstance ? currentInstance.appContext : null! // TODO
this.block = null! // to be set
this.scope = new EffectScope(true)
this.provides = this.refs = EMPTY_OBJ
this.emitted = null
this.ec = null
// init props
this.propsDefaults = null
@ -161,8 +174,10 @@ export class ComponentInstance {
}
}
export function isVaporComponent(value: unknown): value is ComponentInstance {
return value instanceof ComponentInstance
export function isVaporComponent(
value: unknown,
): value is VaporComponentInstance {
return value instanceof VaporComponentInstance
}
export class SetupContext<E = EmitsOptions> {
@ -171,7 +186,7 @@ export class SetupContext<E = EmitsOptions> {
// slots: Readonly<StaticSlots>
expose: (exposed?: Record<string, any>) => void
constructor(instance: ComponentInstance) {
constructor(instance: VaporComponentInstance) {
this.attrs = instance.attrs
// this.emit = instance.emit as EmitFn<E>
// this.slots = instance.slots

View File

@ -1,12 +1,16 @@
import type { ObjectEmitsOptions } from '@vue/runtime-core'
import type { Component } from './component'
import { isArray } from '@vue/shared'
import type { EmitFn, ObjectEmitsOptions } from '@vue/runtime-core'
import {
type VaporComponent,
type VaporComponentInstance,
currentInstance,
} from './component'
import { NOOP, isArray } from '@vue/shared'
/**
* The logic from core isn't too reusable so it's better to duplicate here
*/
export function normalizeEmitsOptions(
comp: Component,
comp: VaporComponent,
): ObjectEmitsOptions | null {
const cached = comp.__emitsOptions
if (cached) return cached
@ -24,3 +28,20 @@ export function normalizeEmitsOptions(
return (comp.__emitsOptions = normalized)
}
export function useEmit(): EmitFn {
if (!currentInstance) {
// TODO warn
return NOOP
} else {
return emit.bind(null, currentInstance)
}
}
export function emit(
instance: VaporComponentInstance,
event: string,
...rawArgs: any[]
): void {
// TODO extract reusable logic from core
}

View File

@ -1,5 +1,5 @@
import { EMPTY_ARR, NO, camelize, hasOwn, isFunction } from '@vue/shared'
import type { Component, ComponentInstance } from './component'
import type { VaporComponent, VaporComponentInstance } from './component'
import {
type NormalizedPropsOptions,
baseNormalizePropsOptions,
@ -18,9 +18,9 @@ type PropSource<T = any> = T | (() => T)
type DynamicPropsSource = PropSource<Record<string, any>>
export function initStaticProps(
comp: Component,
comp: VaporComponent,
rawProps: RawProps | undefined,
instance: ComponentInstance,
instance: VaporComponentInstance,
): boolean {
let hasAttrs = false
const { props, attrs } = instance
@ -85,7 +85,7 @@ export function initStaticProps(
function resolveDefault(
factory: (props: Record<string, any>) => unknown,
instance: ComponentInstance,
instance: VaporComponentInstance,
) {
return factory.call(null, instance.props)
}
@ -96,8 +96,8 @@ function resolveSource(source: PropSource): Record<string, any> {
}
export function getDynamicPropsHandlers(
comp: Component,
instance: ComponentInstance,
comp: VaporComponent,
instance: VaporComponentInstance,
): [ProxyHandler<RawProps>, ProxyHandler<RawProps>] {
if (comp.__propsHandlers) {
return comp.__propsHandlers
@ -204,7 +204,7 @@ export function getDynamicPropsHandlers(
return (comp.__propsHandlers = [propsHandlers, attrsHandlers])
}
function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
function normalizePropsOptions(comp: VaporComponent): NormalizedPropsOptions {
const cached = comp.__propsOptions
if (cached) return cached

View File

@ -1,9 +1,9 @@
import { isArray } from '@vue/shared'
import { type ComponentInstance, isVaporComponent } from './_new/component'
import { type VaporComponentInstance, isVaporComponent } from './_new/component'
export const fragmentKey: unique symbol = Symbol(__DEV__ ? `fragmentKey` : ``)
export type Block = Node | Fragment | ComponentInstance | Block[]
export type Block = Node | Fragment | VaporComponentInstance | Block[]
export type Fragment = {
nodes: Block
anchor?: Node
@ -27,7 +27,7 @@ export function normalizeBlock(block: Block): Node[] {
}
export function findFirstRootElement(
instance: ComponentInstance,
instance: VaporComponentInstance,
): Element | undefined {
const element = getFirstNode(instance.block)
return element instanceof Element ? element : undefined