perf(vapor): improve component instantiation by using class

Mounting 10k components went from ~100ms to ~60ms with this change.
This commit is contained in:
Evan You 2024-12-01 16:41:51 +08:00
parent f0361bafbb
commit 842f94cc73
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
8 changed files with 113 additions and 167 deletions

View File

@ -1,7 +1,6 @@
import { import {
type Component, type Component,
type ComponentInternalInstance, ComponentInternalInstance,
createComponentInstance,
currentInstance, currentInstance,
} from './component' } from './component'
import { setupComponent } from './apiRender' import { setupComponent } from './apiRender'
@ -24,7 +23,7 @@ export function createComponent(
return fallbackComponent(comp, rawProps, slots, current, singleRoot) return fallbackComponent(comp, rawProps, slots, current, singleRoot)
} }
const instance = createComponentInstance( const instance = new ComponentInternalInstance(
comp, comp,
singleRoot ? withAttrs(rawProps) : rawProps, singleRoot ? withAttrs(rawProps) : rawProps,
slots, slots,
@ -42,7 +41,7 @@ export function createComponent(
setupComponent(instance) setupComponent(instance)
// register sub-component with current component for lifecycle management // register sub-component with current component for lifecycle management
current.comps.add(instance) // current.comps.add(instance)
return instance return instance
} }

View File

@ -13,8 +13,7 @@ import {
} from './dom/element' } from './dom/element'
import { type Block, type Fragment, fragmentKey } from './block' import { type Block, type Fragment, fragmentKey } from './block'
import { warn } from './warning' import { warn } from './warning'
import { currentInstance } from './component' import { currentInstance, isVaporComponent } from './component'
import { componentKey } from './component'
import type { DynamicSlot } from './componentSlots' import type { DynamicSlot } from './componentSlots'
import { renderEffect } from './renderEffect' import { renderEffect } from './renderEffect'
@ -382,7 +381,7 @@ function normalizeAnchor(node: Block): Node {
return node return node
} else if (isArray(node)) { } else if (isArray(node)) {
return normalizeAnchor(node[0]) return normalizeAnchor(node[0])
} else if (componentKey in node) { } else if (isVaporComponent(node)) {
return normalizeAnchor(node.block!) return normalizeAnchor(node.block!)
} else { } else {
return normalizeAnchor(node.nodes!) return normalizeAnchor(node.nodes!)

View File

@ -1,8 +1,7 @@
import { NO, getGlobalThis, isFunction, isObject } from '@vue/shared' import { NO, getGlobalThis, isFunction, isObject } from '@vue/shared'
import { import {
type Component, type Component,
type ComponentInternalInstance, ComponentInternalInstance,
createComponentInstance,
validateComponentName, validateComponentName,
} from './component' } from './component'
import { warn } from './warning' import { warn } from './warning'
@ -126,7 +125,7 @@ export function createVaporApp(
container.textContent = '' container.textContent = ''
} }
instance = createComponentInstance( instance = new ComponentInternalInstance(
rootComponent, rootComponent,
rootProps, rootProps,
null, null,

View File

@ -1,9 +1,9 @@
import { import {
type ComponentInternalInstance, type ComponentInternalInstance,
componentKey,
createSetupContext, createSetupContext,
getAttrsProxy, getAttrsProxy,
getSlotsProxy, getSlotsProxy,
isVaporComponent,
setCurrentInstance, setCurrentInstance,
validateComponentName, validateComponentName,
} from './component' } from './component'
@ -60,9 +60,9 @@ export function setupComponent(instance: ComponentInternalInstance): void {
if ( if (
stateOrNode && stateOrNode &&
(stateOrNode instanceof Node || (stateOrNode instanceof Node ||
isVaporComponent(stateOrNode) ||
isArray(stateOrNode) || isArray(stateOrNode) ||
fragmentKey in stateOrNode || fragmentKey in stateOrNode)
componentKey in stateOrNode)
) { ) {
block = stateOrNode block = stateOrNode
} else if (isObject(stateOrNode)) { } else if (isObject(stateOrNode)) {

View File

@ -1,5 +1,5 @@
import { isArray } from '@vue/shared' import { isArray } from '@vue/shared'
import { type ComponentInternalInstance, componentKey } from './component' import { type ComponentInternalInstance, isVaporComponent } from './component'
export const fragmentKey: unique symbol = Symbol(__DEV__ ? `fragmentKey` : ``) export const fragmentKey: unique symbol = Symbol(__DEV__ ? `fragmentKey` : ``)
@ -17,7 +17,7 @@ export function normalizeBlock(block: Block): Node[] {
nodes.push(block) nodes.push(block)
} else if (isArray(block)) { } else if (isArray(block)) {
block.forEach(child => nodes.push(...normalizeBlock(child))) block.forEach(child => nodes.push(...normalizeBlock(child)))
} else if (componentKey in block) { } else if (isVaporComponent(block)) {
nodes.push(...normalizeBlock(block.block!)) nodes.push(...normalizeBlock(block.block!))
} else if (block) { } else if (block) {
nodes.push(...normalizeBlock(block.nodes)) nodes.push(...normalizeBlock(block.nodes))
@ -34,7 +34,7 @@ export function findFirstRootElement(
} }
export function getFirstNode(block: Block | null): Node | undefined { export function getFirstNode(block: Block | null): Node | undefined {
if (!block || componentKey in block) return if (!block || isVaporComponent(block)) return
if (block instanceof Node) return block if (block instanceof Node) return block
if (isArray(block)) { if (isArray(block)) {
if (block.length === 1) { if (block.length === 1) {

View File

@ -1,11 +1,5 @@
import { EffectScope, isRef } from '@vue/reactivity' import { EffectScope, isRef } from '@vue/reactivity'
import { import { EMPTY_OBJ, isArray, isBuiltInTag, isFunction } from '@vue/shared'
EMPTY_OBJ,
hasOwn,
isArray,
isBuiltInTag,
isFunction,
} from '@vue/shared'
import type { Block } from './block' import type { Block } from './block'
import { import {
type ComponentPropsOptions, type ComponentPropsOptions,
@ -143,12 +137,31 @@ export interface ComponentInternalOptions {
type LifecycleHook<TFn = Function> = TFn[] | null type LifecycleHook<TFn = Function> = TFn[] | null
export const componentKey: unique symbol = Symbol(__DEV__ ? `componentKey` : ``) export let currentInstance: ComponentInternalInstance | null = null
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
const prev = currentInstance
currentInstance = instance
return (): void => {
currentInstance = prev
}
}
export const unsetCurrentInstance = (): void => {
currentInstance && currentInstance.scope.off()
currentInstance = null
}
const emptyAppContext = createAppContext()
let uid = 0
export class ComponentInternalInstance {
vapor = true
export interface ComponentInternalInstance {
[componentKey]: true
uid: number uid: number
vapor: true
appContext: AppContext appContext: AppContext
type: Component type: Component
@ -196,185 +209,121 @@ export interface ComponentInternalInstance {
/** /**
* @internal * @internal
*/ */
[VaporLifecycleHooks.BEFORE_MOUNT]: LifecycleHook // [VaporLifecycleHooks.BEFORE_MOUNT]: LifecycleHook;
bm: LifecycleHook
/** /**
* @internal * @internal
*/ */
[VaporLifecycleHooks.MOUNTED]: LifecycleHook // [VaporLifecycleHooks.MOUNTED]: LifecycleHook;
m: LifecycleHook
/** /**
* @internal * @internal
*/ */
[VaporLifecycleHooks.BEFORE_UPDATE]: LifecycleHook // [VaporLifecycleHooks.BEFORE_UPDATE]: LifecycleHook;
bu: LifecycleHook
/** /**
* @internal * @internal
*/ */
[VaporLifecycleHooks.UPDATED]: LifecycleHook // [VaporLifecycleHooks.UPDATED]: LifecycleHook;
u: LifecycleHook
/** /**
* @internal * @internal
*/ */
[VaporLifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook // [VaporLifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook;
bum: LifecycleHook
/** /**
* @internal * @internal
*/ */
[VaporLifecycleHooks.UNMOUNTED]: LifecycleHook // [VaporLifecycleHooks.UNMOUNTED]: LifecycleHook;
um: LifecycleHook
/** /**
* @internal * @internal
*/ */
[VaporLifecycleHooks.RENDER_TRACKED]: LifecycleHook // [VaporLifecycleHooks.RENDER_TRACKED]: LifecycleHook;
rtc: LifecycleHook
/** /**
* @internal * @internal
*/ */
[VaporLifecycleHooks.RENDER_TRIGGERED]: LifecycleHook // [VaporLifecycleHooks.RENDER_TRIGGERED]: LifecycleHook;
rtg: LifecycleHook
/** /**
* @internal * @internal
*/ */
[VaporLifecycleHooks.ACTIVATED]: LifecycleHook // [VaporLifecycleHooks.ACTIVATED]: LifecycleHook;
a: LifecycleHook
/** /**
* @internal * @internal
*/ */
[VaporLifecycleHooks.DEACTIVATED]: LifecycleHook // [VaporLifecycleHooks.DEACTIVATED]: LifecycleHook;
da: LifecycleHook
/** /**
* @internal * @internal
*/ */
[VaporLifecycleHooks.ERROR_CAPTURED]: LifecycleHook // [VaporLifecycleHooks.ERROR_CAPTURED]: LifecycleHook
/** ec: LifecycleHook
* @internal
*/
// [VaporLifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
}
export let currentInstance: ComponentInternalInstance | null = null constructor(
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
const prev = currentInstance
currentInstance = instance
return (): void => {
currentInstance = prev
}
}
export const unsetCurrentInstance = (): void => {
currentInstance && currentInstance.scope.off()
currentInstance = null
}
const emptyAppContext = createAppContext()
let uid = 0
export function createComponentInstance(
component: Component, component: Component,
rawProps: RawProps | null, rawProps: RawProps | null,
slots: RawSlots | null, slots: RawSlots | null,
once: boolean = false, once: boolean = false,
// application root node only // application root node only
appContext?: AppContext, appContext?: AppContext,
): ComponentInternalInstance { ) {
const parent = getCurrentInstance() this.uid = uid++
const _appContext = const parent = (this.parent = currentInstance)
(parent ? parent.appContext : appContext) || emptyAppContext this.root = parent ? parent.root : this
const _appContext = (this.appContext =
const instance: ComponentInternalInstance = { (parent ? parent.appContext : appContext) || emptyAppContext)
[componentKey]: true, this.block = null
uid: uid++, this.container = null!
vapor: true, this.root = null!
appContext: _appContext, this.scope = new EffectScope(true)
this.provides = parent
block: null, ? parent.provides
container: null!, : Object.create(_appContext.provides)
this.type = component
parent, this.comps = new Set()
root: null!, // set later this.scopeIds = []
this.rawProps = null!
scope: new EffectScope(true /* detached */)!, this.propsOptions = normalizePropsOptions(component)
provides: parent ? parent.provides : Object.create(_appContext.provides), this.emitsOptions = normalizeEmitsOptions(component)
type: component,
comps: new Set(),
scopeIds: [],
// resolved props and emits options
rawProps: null!, // set later
propsOptions: normalizePropsOptions(component),
emitsOptions: normalizeEmitsOptions(component),
// state // state
setupState: EMPTY_OBJ, this.setupState = EMPTY_OBJ
setupContext: null, this.setupContext = null
props: EMPTY_OBJ, this.props = EMPTY_OBJ
emit: null!, this.emit = emit.bind(null, this)
emitted: null, this.emitted = null
attrs: EMPTY_OBJ, this.attrs = EMPTY_OBJ
slots: EMPTY_OBJ, this.slots = EMPTY_OBJ
refs: EMPTY_OBJ, this.refs = EMPTY_OBJ
// lifecycle // lifecycle
isMounted: false, this.isMounted = false
isUnmounted: false, this.isUnmounted = false
isUpdating: false, this.isUpdating = false
// TODO: registory of provides, appContext, lifecycles, ... this[VaporLifecycleHooks.BEFORE_MOUNT] = null
/** this[VaporLifecycleHooks.MOUNTED] = null
* @internal this[VaporLifecycleHooks.BEFORE_UPDATE] = null
*/ this[VaporLifecycleHooks.UPDATED] = null
[VaporLifecycleHooks.BEFORE_MOUNT]: null, this[VaporLifecycleHooks.BEFORE_UNMOUNT] = null
/** this[VaporLifecycleHooks.UNMOUNTED] = null
* @internal this[VaporLifecycleHooks.RENDER_TRACKED] = null
*/ this[VaporLifecycleHooks.RENDER_TRIGGERED] = null
[VaporLifecycleHooks.MOUNTED]: null, this[VaporLifecycleHooks.ACTIVATED] = null
/** this[VaporLifecycleHooks.DEACTIVATED] = null
* @internal this[VaporLifecycleHooks.ERROR_CAPTURED] = null
*/
[VaporLifecycleHooks.BEFORE_UPDATE]: null,
/**
* @internal
*/
[VaporLifecycleHooks.UPDATED]: null,
/**
* @internal
*/
[VaporLifecycleHooks.BEFORE_UNMOUNT]: null,
/**
* @internal
*/
[VaporLifecycleHooks.UNMOUNTED]: null,
/**
* @internal
*/
[VaporLifecycleHooks.RENDER_TRACKED]: null,
/**
* @internal
*/
[VaporLifecycleHooks.RENDER_TRIGGERED]: null,
/**
* @internal
*/
[VaporLifecycleHooks.ACTIVATED]: null,
/**
* @internal
*/
[VaporLifecycleHooks.DEACTIVATED]: null,
/**
* @internal
*/
[VaporLifecycleHooks.ERROR_CAPTURED]: null,
/**
* @internal
*/
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
}
instance.root = parent ? parent.root : instance
initProps(instance, rawProps, !isFunction(component), once)
initSlots(instance, slots)
instance.emit = emit.bind(null, instance)
return instance initProps(this, rawProps, !isFunction(component), once)
initSlots(this, slots)
}
} }
export function isVaporComponent( export function isVaporComponent(
val: unknown, val: unknown,
): val is ComponentInternalInstance { ): val is ComponentInternalInstance {
return !!val && hasOwn(val, componentKey) return val instanceof ComponentInternalInstance
} }
export function validateComponentName( export function validateComponentName(

View File

@ -25,6 +25,6 @@ export function invokeLifecycle(
} }
function invokeSub() { function invokeSub() {
instance.comps.forEach(comp => invokeLifecycle(comp, lifecycle, cb, post)) // instance.comps.forEach(comp => invokeLifecycle(comp, lifecycle, cb, post))
} }
} }

View File

@ -77,7 +77,7 @@ export const warn = (__DEV__ ? _warn : NOOP) as typeof _warn
export { nextTick } from './scheduler' export { nextTick } from './scheduler'
export { export {
getCurrentInstance, getCurrentInstance,
type ComponentInternalInstance, type ComponentInternalInstance as ComponentInternalInstance,
type Component as Component, type Component as Component,
type ObjectComponent, type ObjectComponent,
type FunctionalComponent, type FunctionalComponent,