wip(vapor): reuse createApp from core

This commit is contained in:
Evan You 2024-12-04 11:54:26 +08:00
parent cc2439c9e6
commit 4fe05bdd74
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
7 changed files with 158 additions and 82 deletions

View File

@ -1,7 +1,8 @@
import {
type Component,
type ComponentInternalInstance,
type ConcreteComponent,
type GenericComponent,
type GenericComponentInstance,
getComponentPublicInstance,
validateComponentName,
} from './component'
@ -18,8 +19,7 @@ import { type Directive, validateDirectiveName } from './directives'
import type { ElementNamespace, RootRenderFunction } from './renderer'
import type { InjectionKey } from './apiInject'
import { warn } from './warning'
import { type VNode, cloneVNode, createVNode } from './vnode'
import type { RootHydrateFunction } from './hydration'
import type { VNode } from './vnode'
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
import { NO, extend, isFunction, isObject } from '@vue/shared'
import type { Data } from '@vue/runtime-shared'
@ -95,11 +95,11 @@ export interface App<HostElement = any> {
// internal, but we need to expose these for the server-renderer and devtools
_uid: number
_component: ConcreteComponent
_component: GenericComponent
_props: Data | null
_container: HostElement | null
_context: AppContext
_instance: ComponentInternalInstance | null
_instance: GenericComponentInstance | null
/**
* @internal custom element vnode
@ -257,15 +257,30 @@ export function createAppContext(): AppContext {
}
export type CreateAppFunction<HostElement> = (
rootComponent: Component,
rootComponent: GenericComponent,
rootProps?: Data | null,
) => App<HostElement>
let uid = 0
export type AppMountFn<HostElement> = (
app: App,
rootContainer: HostElement,
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
) => GenericComponentInstance
export type AppUnmountFn = (app: App) => void
/**
* @internal
*/
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction,
// render: RootRenderFunction<HostElement>,
// hydrate?: RootHydrateFunction,
mount: AppMountFn<HostElement>,
unmount: AppUnmountFn,
render?: RootRenderFunction,
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
@ -369,59 +384,32 @@ export function createAppAPI<HostElement>(
},
mount(
rootContainer: HostElement,
rootContainer: HostElement & { __vue_app__?: App },
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
): any {
if (!isMounted) {
// #5571
if (__DEV__ && (rootContainer as any).__vue_app__) {
if (__DEV__ && rootContainer.__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`,
)
}
const vnode = app._ceVNode || createVNode(rootComponent, rootProps)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}
// HMR root reload
if (__DEV__) {
context.reload = () => {
// casting to ElementNamespace because TS doesn't guarantee type narrowing
// over function boundaries
render(
cloneVNode(vnode),
rootContainer,
namespace as ElementNamespace,
)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, namespace)
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
const instance = mount(app, rootContainer, isHydrate, namespace)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component
app._instance = instance
devtoolsInitApp(app, version)
}
return getComponentPublicInstance(vnode.component!)
isMounted = true
app._container = rootContainer
// for devtools and telemetry
rootContainer.__vue_app__ = app
return getComponentPublicInstance(instance)
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
@ -449,7 +437,7 @@ export function createAppAPI<HostElement>(
app._instance,
ErrorCodes.APP_UNMOUNT_CLEANUP,
)
render(null, app._container)
unmount(app)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = null
devtoolsUnmountApp(app)
@ -485,7 +473,12 @@ export function createAppAPI<HostElement>(
})
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
installAppCompatProperties(
app,
context,
// vapor doesn't have compat mode so this is always passed
render!,
)
}
return app

View File

@ -367,6 +367,9 @@ export interface GenericComponentInstance {
*/
propsDefaults: Data | null
// exposed properties via expose()
exposed: Record<string, any> | null
// lifecycle
isMounted: boolean
isUnmounted: boolean
@ -519,8 +522,7 @@ export interface ComponentInternalInstance extends GenericComponentInstance {
data: Data // options API only
emit: EmitFn
slots: InternalSlots
// exposed properties via expose()
exposed: Record<string, any> | null
exposeProxy: Record<string, any> | null
/**
@ -1228,24 +1230,33 @@ export function createSetupContext(
}
export function getComponentPublicInstance(
instance: ComponentInternalInstance,
instance: GenericComponentInstance,
): ComponentPublicInstance | ComponentInternalInstance['exposed'] | null {
if (instance.exposed) {
return (
instance.exposeProxy ||
(instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
get(target, key: string) {
if (key in target) {
return target[key]
} else if (key in publicPropertiesMap) {
return publicPropertiesMap[key](instance)
}
},
has(target, key: string) {
return key in target || key in publicPropertiesMap
},
}))
)
if ('exposeProxy' in instance) {
return (
instance.exposeProxy ||
(instance.exposeProxy = new Proxy(
proxyRefs(markRaw(instance.exposed)),
{
get(target, key: string) {
if (key in target) {
return target[key]
} else if (key in publicPropertiesMap) {
return publicPropertiesMap[key](
instance as ComponentInternalInstance,
)
}
},
has(target, key: string) {
return key in target || key in publicPropertiesMap
},
},
))
)
} else {
return instance.exposed
}
} else {
return instance.proxy
}

View File

@ -501,3 +501,8 @@ export {
nextUid,
} from './component'
export { pushWarningContext, popWarningContext } from './warning'
export {
createAppAPI,
type AppMountFn,
type AppUnmountFn,
} from './apiCreateApp'

View File

@ -8,6 +8,7 @@ import {
type VNodeHook,
type VNodeProps,
cloneIfMounted,
cloneVNode,
createVNode,
invokeVNodeHook,
isSameVNodeType,
@ -57,7 +58,12 @@ import {
import { updateProps } from './componentProps'
import { updateSlots } from './componentSlots'
import { popWarningContext, pushWarningContext, warn } from './warning'
import { type CreateAppFunction, createAppAPI } from './apiCreateApp'
import {
type AppMountFn,
type AppUnmountFn,
type CreateAppFunction,
createAppAPI,
} from './apiCreateApp'
import { setRef } from './rendererTemplateRef'
import {
type SuspenseBoundary,
@ -2397,10 +2403,49 @@ function baseCreateRenderer(
)
}
const mountApp: AppMountFn<Element> = (
app,
container,
isHydrate,
namespace,
) => {
const vnode = app._ceVNode || createVNode(app._component, app._props)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = app._context
if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}
// HMR root reload
if (__DEV__) {
app._context.reload = () => {
// casting to ElementNamespace because TS doesn't guarantee type narrowing
// over function boundaries
render(cloneVNode(vnode), container, namespace as ElementNamespace)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, container as any)
} else {
render(vnode, container, namespace)
}
return vnode.component!
}
const unmountApp: AppUnmountFn = app => {
render(null, app._container)
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate),
createApp: createAppAPI(mountApp, unmountApp, render),
}
}

View File

@ -1,5 +1,6 @@
import {
type App,
type ConcreteComponent,
type CreateAppFunction,
type DefineComponent,
DeprecationTypes,
@ -108,7 +109,7 @@ export const createApp = ((...args) => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
const component = app._component as ConcreteComponent
if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
@ -225,7 +226,10 @@ function injectCompilerOptionsCheck(app: App) {
}
}
function normalizeContainer(
/**
* @internal
*/
export function normalizeContainer(
container: Element | ShadowRoot | string,
): Element | ShadowRoot | null {
if (isString(container)) {

View File

@ -1,18 +1,36 @@
import { normalizeContainer } from '../apiRender'
import { insert } from '../dom/element'
import { type VaporComponent, createComponent } from './component'
import {
type AppMountFn,
type AppUnmountFn,
type CreateAppFunction,
createAppAPI,
} from '@vue/runtime-core'
let _createApp: CreateAppFunction<ParentNode>
const mountApp: AppMountFn<ParentNode> = (app, container) => {
// clear content before mounting
if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
container.textContent = ''
}
const instance = createComponent(app._component)
insert(instance.block, container)
return instance
}
const unmountApp: AppUnmountFn = app => {
// TODO
}
export function createVaporApp(comp: VaporComponent): any {
return {
mount(container: string | ParentNode) {
container = normalizeContainer(container)
// clear content before mounting
if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
container.textContent = ''
}
const instance = createComponent(comp)
insert(instance.block, container)
return instance
},
if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp)
const app = _createApp(comp)
const mount = app.mount
app.mount = (container, ...args: any[]) => {
container = normalizeContainer(container) // TODO reuse from runtime-dom
return mount(container, ...args)
}
return app
}

View File

@ -143,7 +143,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
rawProps: RawProps | undefined
props: Record<string, any>
attrs: Record<string, any>
exposed?: Record<string, any>
exposed: Record<string, any> | null
emitted: Record<string, boolean> | null
propsDefaults: Record<string, any> | null
@ -178,7 +178,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
this.rawProps = rawProps
this.provides = this.refs = EMPTY_OBJ
this.emitted = this.ec = null
this.emitted = this.ec = this.exposed = null
this.isMounted = this.isUnmounted = this.isDeactivated = false
// init props