前面几篇介绍的大都是reactivity相关的API。我们在使用Vue3作为前端框架时,往往在我们的main.js/main.ts里来创建vue3app实例,就会用到createApp这个API。本篇就来简要了解一下createApp里发生的故事。

一、相关 ts 类型

可以先瞄一眼与createAppApi相关的ts类型,这样就更能理解它的使用,这里挑几个简要介绍一下。

1. App

AppcreateApp返回值的类型,可以看到项目里常用的一些方法都在这里,某些方法返回了this,则可以链式调用。此外,还兼容了vue2filter,还有一些内部的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export interface App<HostElement = any> {
version: string;
// config 上有常用到的 globalProperties
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(
rootContainer: HostElement | string,
isHydrate?: boolean,
isSVG?: boolean
): ComponentPublicInstance;
unmount(): void;
provide<T>(key: InjectionKey<T> | string, value: T): this;

// internal, but we need to expose these for the server-renderer and devtools
_uid: number;
_component: ConcreteComponent;
_props: Data | null;
_container: HostElement | null;
_context: AppContext;
_instance: ComponentInternalInstance | null;

/**
* v2 compat only
*/
filter?(name: string): Function | undefined;
filter?(name: string, filter: Function): this;

/**
* @internal v3 compat only
*/
_createRoot?(options: ComponentOptions): ComponentPublicInstance;
}

2. AppConfig

创建的App的配置,包含的内容在vue2里基本都有,重要的例如组件合并策略optionMergeStrategiesVue全局属性globalProperties(Vue2里直接挂到原型上)、编译器选项compilerOptions、错误与告警处理程序等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export interface AppConfig {
// @private
readonly isNativeTag?: (tag: string) => boolean;

performance: boolean;
optionMergeStrategies: Record<string, OptionMergeFunction>;
globalProperties: Record<string, any>;
errorHandler?: (
err: unknown,
instance: ComponentPublicInstance | null,
info: string
) => void;
warnHandler?: (
msg: string,
instance: ComponentPublicInstance | null,
trace: string
) => void;

/**
* Options to pass to `@vue/compiler-dom`.
* Only supported in runtime compiler build.
*/
compilerOptions: RuntimeCompilerOptions;

/**
* @deprecated use config.compilerOptions.isCustomElement
*/
isCustomElement?: (tag: string) => boolean;

/**
* Temporary config for opt-in to unwrap injected refs.
* TODO deprecate in 3.3
*/
unwrapInjectedRef?: boolean;
}

3. AppContext

App的上下文,包含了对于componentsdirectivesmixinsprovidesconfig记录、对于propsemits的缓存、用于热更新的reload方法、兼容vue2filters记录等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export interface AppContext {
app: App; // for devtools
config: AppConfig;
mixins: ComponentOptions[];
components: Record<string, Component>;
directives: Record<string, Directive>;
provides: Record<string | symbol, any>;

/**
* Cache for merged/normalized component options
* Each app instance has its own cache because app-level global mixins and
* optionMergeStrategies can affect merge behavior.
* @internal
*/
optionsCache: WeakMap<ComponentOptions, MergedComponentOptions>;
/**
* Cache for normalized props options
* @internal
*/
propsCache: WeakMap<ConcreteComponent, NormalizedPropsOptions>;
/**
* Cache for normalized emits options
* @internal
*/
emitsCache: WeakMap<ConcreteComponent, ObjectEmitsOptions | null>;
/**
* HMR only
* @internal
*/
reload?: () => void;
/**
* v2 compat only
* @internal
*/
filters?: Record<string, Function>;
}

4. Plugin

PluginPlugin中的install方法,基本和vue2一致。清晰可见,Plugin可以本身就是一个PluginInstallFunction类型函数,也可以是一个包含该类型函数的对象。

1
2
3
4
5
6
7
type PluginInstallFunction = (app: App, ...options: any[]) => any;

export type Plugin =
| (PluginInstallFunction & { install?: PluginInstallFunction })
| {
install: PluginInstallFunction;
};

5. CreateAppFunction

CreateAppFunction就是我们的createApp函数的类型,接收一个根组件,以及一个可选参数rootProps对根组件进行传参。

1
2
3
4
export type CreateAppFunction<HostElement> = (
rootComponent: Component,
rootProps?: Data | null
) => App<HostElement>;

二、createApp

从某种程度上可以说,Vue3的一切都是从createApp开始的。createApp这个API定义在packages/runtime-dom/src/index.ts文件里,接下来简要看一看它大致走了哪些流程。

1. createApp

  • 首先在ensureRenderer中调用createRenderer得到rendererrenderer上有createApp的方法,从而得到app
  • 重写app.mount方法,对app._componentcontainer的内容作处理;并且在其中调用原本的mount之前,先对container的内容进行清空。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
);
}

export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args);

if (__DEV__) {
injectNativeTagCheck(app);
injectCompilerOptionsCheck(app);
}

const { mount } = app;
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector);
if (!container) return;

const component = app._component;
if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's
// rendered by the server, the template should not contain any user data.
component.template = container.innerHTML;
// 2.x compat check
if (__COMPAT__ && __DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i];
if (attr.name !== "v-cloak" && /^(v-|:|@)/.test(attr.name)) {
compatUtils.warnDeprecation(
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
null
);
break;
}
}
}
}

// clear content before mounting
container.innerHTML = "";
const proxy = mount(container, false, container instanceof SVGElement);
if (container instanceof Element) {
container.removeAttribute("v-cloak");
container.setAttribute("data-v-app", "");
}
return proxy;
};

return app;
}) as CreateAppFunction<Element>;

那么这里就会产生疑问,毕竟真正的createApp是在 renderer 上的。而renderer来自createRenderer,那么这个createRenderer又是如何创建renderer的呢?

1
createApp() -> ensureRenderer() -> createRenderer() => renderer -> renderer.createApp()

2. createRenderer

我们可以在packages/runtime-core/src/renderer.ts里找到createRenderer的定义。发现是调用了baseCreateRenderer。这个方法就比较长了,加上重载的话有 2000+行,其中包含了patchmoveunmount等许多diff相关的方法,目前就不在这里展开了,只看一下它的返回值,追踪一下我们说的createApp的来源。可以看到,返回的renderer对象上有renderhydrate方法和createApphydrate是用于baseCreateRenderer的另一种重载,render方法就非常重要了,而createApp的来源是createAppAPI,这个API定义在packages/runtime-core/src/createAppAPI.ts里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options);
}

// baseCreateRenderer

function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 此处省略上万字

// render方法,虽重要,但不是本文的主角,先露个脸吧
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true);
}
} else {
patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
isSVG
);
}
flushPostFlushCbs();
container._vnode = vnode;
};

// ...

return {
render,
hydrate,
createApp: createAppAPI(render, hydrate),
};
}

3. createAppAPI

好家伙,走了这么长个流程,终于轮到主角登场了。在baseCreateRenderer的返回值中,我们可以看到,createApp方法就是以renderhydrate作为入参,提供给createAppAPI,从而诞生的。而这个近200行的函数,直接返回了我们用的createApp这个函数,这下子终于得到了真正的createApp。而逻辑也非常简单清晰:

  • 创建上下文context
  • 声明一个不可重复的插件容器;
  • 初始化isMounted状态为false
  • 创建app并挂到context上,最后返回app
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = { ...rootComponent };
}

if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`);
rootProps = null;
}

// 创建上下文
const context = createAppContext();
// 插件容器
const installedPlugins = new Set();

// 还没进行 mount() 呢,isMounted 自然是 false
let isMounted = false;

// 终于创建了app了
const app: App = (context.app = {
// ...
});

// 考虑兼容的属性
if (__COMPAT__) {
installAppCompatProperties(app, context, render);
}

return app;
};
}

那么重点就是创建的app了,让我们掀开它神秘的面纱。

  • 配置了一些内部属性;
  • 利用存取器配置了只读的config属性;
  • 定义了一些方法,如usemountcomponentdirectivemixinunmountprovide等,这时候回顾一下最开始我们说到的相关ts类型中的App类型,就对应上了。
  • 其中,componentdirectivemixinprovide都是用于定义一些全局可用的东西。这几个方法的逻辑也都一致,把定义的全局的内容添加到上下文context对象的相应字段中。
  • use:这个应该家喻户晓了,就是使用插件,调用其中的install方法或者插件本身(当插件本身就是一个函数且没有install方法时) 来安装插件,并且用installedPlugins来判断是否已安装;
  • mount:根据闭包的变量isMounted来判断app是否已经挂载;用根组件rootComponent作为参数,调用createVnode来生成根节点,并将上下文context也保存在vnode.appContext中;执行render函数将vnode渲染到rootContainer中,这一步我们应该很熟,就是替换innerHTML;之后变更isMounted状态为true等;
  • unmount:同样是执行render函数,只是这次是把null空值渲染到rootContainer中,用空的内容替换之前mount时渲染的内容,从而达到卸载应用的效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,

version,

get config() {
return context.config
},

set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},

use(plugin: Plugin, ...options: any[]) {
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)
} else if (plugin && isFunction(plugin.install)) {
installedPlugins.add(plugin)
plugin.install(app, ...options)
} else if (isFunction(plugin)) {
installedPlugins.add(plugin)
plugin(app, ...options)
} else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`
)
}
return app
},

mixin(mixin: ComponentOptions) {
if (__FEATURE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin)
} else if (__DEV__) {
warn(
'Mixin has already been applied to target app' +
(mixin.name ? `: ${mixin.name}` : '')
)
}
} else if (__DEV__) {
warn('Mixins are only available in builds supporting Options API')
}
return app
},

component(name: string, component?: Component): any {
if (__DEV__) {
validateComponentName(name, context.config)
}
if (!component) {
return context.components[name]
}
if (__DEV__ && context.components[name]) {
warn(`Component "${name}" has already been registered in target app.`)
}
context.components[name] = component
return app
},

directive(name: string, directive?: Directive) {
if (__DEV__) {
validateDirectiveName(name)
}

if (!directive) {
return context.directives[name] as any
}
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`)
}
context.directives[name] = directive
return app
},

mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// #5571
if (__DEV__ && (rootContainer as any).__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 = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context

// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}

if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app

if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component
devtoolsInitApp(app, version)
}

return getExposeProxy(vnode.component!) || vnode.component!.proxy
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. `const createMyApp = () => createApp(App)``
)
}
},

unmount() {
if (isMounted) {
render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = null
devtoolsUnmountApp(app)
}
delete app._container.__vue_app__
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
},

provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`
)
}

context.provides[key as string | symbol] = value

return app
}
})

createApp流程基本都弄明白了,但是我们并不清楚render的过程是如何进行的。后续会抽时间解读render函数的故事。