feat: createApp / appContext

This commit is contained in:
Evan You 2019-09-02 16:09:34 -04:00
parent aac807bc63
commit 32713f8fce
8 changed files with 226 additions and 9 deletions

View File

@ -0,0 +1,168 @@
import {
ComponentOptions,
Component,
ComponentRenderProxy,
Data,
ComponentInstance
} from './component'
import { Directive } from './directives'
import { HostNode, RootRenderFunction } from './createRenderer'
import { InjectionKey } from './apiInject'
import { isFunction } from '@vue/shared'
import { warn } from './warning'
import { createVNode } from './vnode'
export interface App {
config: AppConfig
use(plugin: Plugin, options?: any): this
mixin(mixin: ComponentOptions): this
component(name: string): Component | undefined
component(name: string, component: Component): this
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): this
mount(
rootComponent: Component,
rootContainer: string | HostNode,
rootProps?: Data
): ComponentRenderProxy
provide<T>(key: InjectionKey<T> | string, value: T): void
}
export interface AppConfig {
silent: boolean
devtools: boolean
performance: boolean
errorHandler?: (
err: Error,
instance: ComponentRenderProxy,
info: string
) => void
warnHandler?: (
msg: string,
instance: ComponentRenderProxy,
trace: string
) => void
ignoredElements: Array<string | RegExp>
keyCodes: Record<string, number | number[]>
optionMergeStrategies: {
[key: string]: (
parent: any,
child: any,
instance: ComponentRenderProxy
) => any
}
}
export interface AppContext {
config: AppConfig
mixins: ComponentOptions[]
components: Record<string, Component>
directives: Record<string, Directive>
provides: Record<string | symbol, any>
}
type PluginInstallFunction = (app: App) => any
type Plugin =
| PluginInstallFunction
| {
install: PluginInstallFunction
}
export function createAppContext(): AppContext {
return {
config: {
silent: false,
devtools: true,
performance: false,
errorHandler: undefined,
warnHandler: undefined,
ignoredElements: [],
keyCodes: {},
optionMergeStrategies: {}
},
mixins: [],
components: {},
directives: {},
provides: {}
}
}
export function createAppAPI(render: RootRenderFunction): () => App {
return function createApp(): App {
const context = createAppContext()
const app: App = {
get config() {
return context.config
},
set config(v) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
},
use(plugin: Plugin) {
if (isFunction(plugin)) {
plugin(app)
} else if (isFunction(plugin.install)) {
plugin.install(app)
} else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`
)
}
return app
},
mixin(mixin: ComponentOptions) {
context.mixins.push(mixin)
return app
},
component(name: string, component?: Component) {
// TODO component name validation
if (!component) {
return context.components[name] as any
} else {
context.components[name] = component
return app
}
},
directive(name: string, directive?: Directive) {
// TODO directive name validation
if (!directive) {
return context.directives[name] as any
} else {
context.directives[name] = directive
return app
}
},
mount(rootComponent, rootContainer, rootProps?: Data) {
const vnode = createVNode(rootComponent, rootProps)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
render(vnode, rootContainer)
return (vnode.component as ComponentInstance)
.renderProxy as ComponentRenderProxy
},
provide(key, value) {
if (__DEV__ && key in context.provides) {
warn(
`App already provides property with key "${key}". ` +
`It will be overwritten with the new value.`
)
}
context.provides[key as any] = value
}
}
return app
}
}

View File

@ -13,6 +13,7 @@ import {
callWithErrorHandling, callWithErrorHandling,
callWithAsyncErrorHandling callWithAsyncErrorHandling
} from './errorHandling' } from './errorHandling'
import { AppContext, createAppContext } from './apiCreateApp'
export type Data = { [key: string]: unknown } export type Data = { [key: string]: unknown }
@ -79,6 +80,8 @@ export interface FunctionalComponent<P = {}> {
displayName?: string displayName?: string
} }
export type Component = ComponentOptions | FunctionalComponent
type LifecycleHook = Function[] | null type LifecycleHook = Function[] | null
export const enum LifecycleHooks { export const enum LifecycleHooks {
@ -107,6 +110,7 @@ interface SetupContext {
export type ComponentInstance<P = Data, S = Data> = { export type ComponentInstance<P = Data, S = Data> = {
type: FunctionalComponent | ComponentOptions type: FunctionalComponent | ComponentOptions
parent: ComponentInstance | null parent: ComponentInstance | null
appContext: AppContext
root: ComponentInstance root: ComponentInstance
vnode: VNode vnode: VNode
next: VNode | null next: VNode | null
@ -184,6 +188,8 @@ export function createComponent(options: any) {
return isFunction(options) ? { setup: options } : (options as any) return isFunction(options) ? { setup: options } : (options as any)
} }
const emptyAppContext = createAppContext()
export function createComponentInstance( export function createComponentInstance(
vnode: VNode, vnode: VNode,
parent: ComponentInstance | null parent: ComponentInstance | null
@ -191,6 +197,9 @@ export function createComponentInstance(
const instance = { const instance = {
vnode, vnode,
parent, parent,
// inherit parent app context - or - if root, adopt from root vnode
appContext:
(parent ? parent.appContext : vnode.appContext) || emptyAppContext,
type: vnode.type as any, type: vnode.type as any,
root: null as any, // set later so it can point to itself root: null as any, // set later so it can point to itself
next: null, next: null,

View File

@ -93,7 +93,12 @@ export interface RendererOptions {
querySelector(selector: string): HostNode | null querySelector(selector: string): HostNode | null
} }
export function createRenderer(options: RendererOptions) { export type RootRenderFunction = (
vnode: VNode | null,
dom: HostNode | string
) => void
export function createRenderer(options: RendererOptions): RootRenderFunction {
const { const {
insert: hostInsert, insert: hostInsert,
remove: hostRemove, remove: hostRemove,
@ -1152,8 +1157,31 @@ export function createRenderer(options: RendererOptions) {
} }
} }
return function render(vnode: VNode | null, dom: HostNode): VNode | null { return function render(vnode: VNode | null, dom: HostNode | string) {
if (isString(dom)) {
if (isFunction(hostQuerySelector)) {
dom = hostQuerySelector(dom)
if (!dom) {
if (__DEV__) {
warn(
`Failed to locate root container: ` +
`querySelector returned null.`
)
}
return
}
} else {
if (__DEV__) {
warn(
`Failed to locate root container: ` +
`target platform does not support querySelector.`
)
}
return
}
}
if (vnode == null) { if (vnode == null) {
debugger
if (dom._vnode) { if (dom._vnode) {
unmount(dom._vnode, null, true) unmount(dom._vnode, null, true)
} }
@ -1161,7 +1189,7 @@ export function createRenderer(options: RendererOptions) {
patch(dom._vnode, vnode, dom) patch(dom._vnode, vnode, dom)
} }
flushPostFlushCbs() flushPostFlushCbs()
return (dom._vnode = vnode) dom._vnode = vnode
} }
} }

View File

@ -21,6 +21,7 @@ import {
ComponentRenderProxy ComponentRenderProxy
} from './component' } from './component'
import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling' import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling'
import { HostNode } from './createRenderer'
export interface DirectiveBinding { export interface DirectiveBinding {
instance: ComponentRenderProxy | null instance: ComponentRenderProxy | null
@ -31,7 +32,7 @@ export interface DirectiveBinding {
} }
export type DirectiveHook = ( export type DirectiveHook = (
el: any, el: HostNode,
binding: DirectiveBinding, binding: DirectiveBinding,
vnode: VNode, vnode: VNode,
prevVNode: VNode | null prevVNode: VNode | null

View File

@ -28,6 +28,7 @@ export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
export { getCurrentInstance } from './component' export { getCurrentInstance } from './component'
// For custom renderers // For custom renderers
export { createAppAPI } from './apiCreateApp'
export { createRenderer } from './createRenderer' export { createRenderer } from './createRenderer'
export { export {
handleError, handleError,

View File

@ -12,6 +12,7 @@ import { RawSlots } from './componentSlots'
import { PatchFlags } from './patchFlags' import { PatchFlags } from './patchFlags'
import { ShapeFlags } from './shapeFlags' import { ShapeFlags } from './shapeFlags'
import { isReactive } from '@vue/reactivity' import { isReactive } from '@vue/reactivity'
import { AppContext } from './apiCreateApp'
export const Fragment = Symbol('Fragment') export const Fragment = Symbol('Fragment')
export const Text = Symbol('Text') export const Text = Symbol('Text')
@ -50,6 +51,9 @@ export interface VNode {
patchFlag: number patchFlag: number
dynamicProps: string[] | null dynamicProps: string[] | null
dynamicChildren: VNode[] | null dynamicChildren: VNode[] | null
// application root node only
appContext: AppContext | null
} }
// Since v-if and v-for are the two possible ways node structure can dynamically // Since v-if and v-for are the two possible ways node structure can dynamically
@ -152,7 +156,8 @@ export function createVNode(
shapeFlag, shapeFlag,
patchFlag, patchFlag,
dynamicProps, dynamicProps,
dynamicChildren: null dynamicChildren: null,
appContext: null
} }
normalizeChildren(vnode, children) normalizeChildren(vnode, children)
@ -192,6 +197,7 @@ export function cloneVNode(vnode: VNode): VNode {
patchFlag: vnode.patchFlag, patchFlag: vnode.patchFlag,
dynamicProps: vnode.dynamicProps, dynamicProps: vnode.dynamicProps,
dynamicChildren: vnode.dynamicChildren, dynamicChildren: vnode.dynamicChildren,
appContext: vnode.appContext,
// these should be set to null since they should only be present on // these should be set to null since they should only be present on
// mounted VNodes. If they are somehow not null, this means we have // mounted VNodes. If they are somehow not null, this means we have

View File

@ -1,11 +1,13 @@
import { createRenderer, VNode } from '@vue/runtime-core' import { createRenderer, VNode, createAppAPI } from '@vue/runtime-core'
import { nodeOps } from './nodeOps' import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp' import { patchProp } from './patchProp'
export const render = createRenderer({ export const render = createRenderer({
patchProp, patchProp,
...nodeOps ...nodeOps
}) as (vnode: VNode | null, container: HTMLElement) => VNode }) as (vnode: VNode | null, container: HTMLElement) => void
export const createApp = createAppAPI(render)
// re-export everything from core // re-export everything from core
// h, Component, reactivity API, nextTick, flags & types // h, Component, reactivity API, nextTick, flags & types

View File

@ -1,4 +1,4 @@
import { createRenderer, VNode } from '@vue/runtime-core' import { createRenderer, VNode, createAppAPI } from '@vue/runtime-core'
import { nodeOps, TestElement } from './nodeOps' import { nodeOps, TestElement } from './nodeOps'
import { patchProp } from './patchProp' import { patchProp } from './patchProp'
import { serializeInner } from './serialize' import { serializeInner } from './serialize'
@ -6,7 +6,9 @@ import { serializeInner } from './serialize'
export const render = createRenderer({ export const render = createRenderer({
patchProp, patchProp,
...nodeOps ...nodeOps
}) as (node: VNode | null, container: TestElement) => VNode }) as (node: VNode | null, container: TestElement) => void
export const createApp = createAppAPI(render)
// convenience for one-off render validations // convenience for one-off render validations
export function renderToString(vnode: VNode) { export function renderToString(vnode: VNode) {