createApp - vue3源码解读
theme: fancy一. 前言vue3 已经成为主流,vue3 对 vue2 做了兼容的基础上,增加了大量响应式API(hook),更改了生命周期钩子,对响应式原理也做了优化,用 proxy 代替了之前的defineProperty,同时使用createApp的方式代替了之前使用new来启动的方式。好了,进入今天的主题,让我们看下createApp的由来以及它内部发生了什么?二. create
theme: fancy
一. 前言
vue3 已经成为主流,vue3 对 vue2 做了兼容的基础上,增加了大量响应式API(hook)
,更改了生命周期钩子,对响应式原理也做了优化,用 proxy
代替了之前的defineProperty
,同时使用createApp
的方式代替了之前使用new
来启动的方式。好了,进入今天的主题,让我们看下createApp
的由来以及它内部发生了什么?
二. createApp 的创建
1. creatAppAPI
该方法接收两个参数,并且返回一个createApp,也就是我们在启动时使用的接口,我们先不关心该参数从哪来,我们只要知道它的作用即可,接着往下看
//源码路径 core/packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp (){
//...
}
}
2. 参数 render
我们想了解它的作用,首先看一下它的类型RootRenderFunction
//源码路径 core/packages/runtime-core/src/renderer.ts
export type RootRenderFunction<HostElement = RendererElement> = (
vnode: VNode | null,
container: HostElement,
isSVG?: boolean
) => void
从上述源码中我们可以了解到,render
参数是一个Function
类型,接收两个参数,一个是VNode
虚拟dom,一个是container
真实的dom元素,由此我们可以预想,render
的作用是:将虚拟dom渲染到真实的dom中
3. 参数 hydrate
该参数的作用是用于服务器渲染,此文先不做过多讲解。
三. createApp 内部执行
该方法最终返回一个app
,类似于我们vue2中的vm
实例,让我们先来总结下app
内部有些什么参数吧(仅展示部分源码,详细实现往下看)
//源码路径 core/packages/runtime-core/src/apiCreateApp.ts
function createApp(rootComponent, rootProps = null) {
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
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) {
},
use(plugin: Plugin, ...options: any[]) {
},
mixin(mixin: ComponentOptions) {
},
component(name: string, component?: Component): any {
},
directive(name: string, directive?: Directive) {
},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
},
unmount() {
},
provide(key, value) {
}
})
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
return app
}
1. app
- uid:用于标识组件唯一id
- _comonent:存放当前组件通过编译后的数据
- _props:当前组件接受的参数
- _container:当前组件对应的要渲染的真实dom位置
- _context:当前组件上下文对象,其中包含config,app等
- _instance:当前组件实例对象
- config: 配置信息,也就是 context中的config信息,存放一些全局信息
- installedPlugins:用来保存安装过的插件,使用
Set
代替了之前的数组
,有效避免了重复安装 - use:用来注册插件
- mixin:用来混入全局数据
- component:用于注册组件
- mount:执行挂载操作
- provide:为子组件提供可用参数
接下来让我们一个个分析其详细实现。
config
config参数此处用了数据劫持的方式来设置获取和设置值,类似与Object.defineProperty
,在 getter 中取到的是 context.config 的值,在 setter 中我们可以知道,app.config是不可以直接更改的,只可以改变其中的参数,让我们来看下其内部定义,接着往下看
//源码路径 core/packages/runtime-core/src/apiCreateApp.ts
get config() {
return context.config
},
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
_context
该参数是由createAppContext
方法返回,让我们看下其内部实现
该方法很简单,直接返回了一个对象,其中包括 app,config等。
通过对比vue2
,在vue2
中我们将组件的信息保存在vm.$options
中,而vue3
将所有数据存放在app._context
中。
// 源码路径 core/packages/runtime-core/src/apiCreateApp.ts
function createAppContext(): AppContext {
return {
app: null as any,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {}
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null),
optionsCache: new WeakMap(),
propsCache: new WeakMap(),
emitsCache: new WeakMap()
}
}
use
该方法用来注册插件,将插件保存到installedPlugins
中,该变量是一个Set
类型,首先通过installedPlugins.has
来判断插件是否已经安装过,避免重复安装浪费性能,之后使用installedPlugins.add
将插件存放起来,同时调用Plugin.install
方法,将app
作为参数导入,之后返回app
,因此实现了链式调用。
// 源码路径 core/packages/runtime-core/src/apiCreateApp.ts
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
该方法是向该组件的所有子组件中混入相同的options
,其实现也比较简单,将传入的options存入context
中的mixins数组中,传入前检测数组中是否已经存在。该方法不同于vue2
中mixin
的实现,vue2
是将options混入到vue
或者vueComponent
的静态options当中。感兴趣的可以看下我的另一篇文章Vue.mixin 源码深入理解 - 掘金 (juejin.cn)。
// 源码路径 core/packages/runtime-core/src/apiCreateApp.ts
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
该方法用来注册组件,接收两个参数,第一个为组件名字,第二个参数是对应的组件,我们将其组件保存到context.components
中,context.components
是一个对象,键和值对应着我们的组件名和组件。
// 源码路径 core/packages/runtime-core/src/apiCreateApp.ts
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
},
provide
该方法接收两个参数,要提供给子组件的数据对应的键值,保存到context.provides
对象中。
// 源码路径 core/packages/runtime-core/src/apiCreateApp.ts
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] = value
return app
}
unmount
该方法用来卸载组件,通过isMounted
来过滤,只有已经过载的组件,才可以继续执行,在此处调用了render
函数,我们在上文中提过,忘记的可以返回去看一下。此处传入一个空字符串,来替换该组件的内容,实现页面卸载。
// 源码路径 core/packages/runtime-core/src/apiCreateApp.ts
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.`)
}
},
mount
该API是最重要的,所以我们也放在压轴位置来讲。
该方法传入三个参数,但是我们只讲第一个参数rootContainer
,该参数值是一个真实dom元素,但是我们在使用时只是传入一个idapp.mount('#app')
,而获取dom的过程则是在编译器compiler
中完成。因此我们打印参数rootContainer的值并不是#app
,而是一个真实的dom元素。
该方法内部,主要执行了两个API,先通过createVNode
将我们的根组件App
转换成VNode
,然后执行render
将虚拟dom渲染为真实dom。
createVNode
和VNode
的具体实现将在后续文章中专门讲解,在此不做详细解释。
// 源码路径 core/packages/runtime-core/src/apiCreateApp.ts
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
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
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG) // 其中执行了 setup
}
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)\``
)
}
},
四.总结
到此,我们的createApp
从构建到执行,到组件挂载全部执行完毕。大致过程如下:
生
成
r
e
n
d
e
r
−
>
c
r
e
a
t
e
A
p
p
A
P
I
−
>
c
r
e
a
t
e
A
p
p
−
>
初
始
化
并
构
建
a
p
p
−
>
执
行
a
p
p
.
m
o
u
n
t
−
>
c
r
e
a
t
e
V
N
o
d
e
−
>
r
e
n
d
e
r
生成render -> createAppAPI -> createApp ->初始化并构建app -> 执行app.mount -> createVNode -> render
生成render−>createAppAPI−>createApp−>初始化并构建app−>执行app.mount−>createVNode−>render
由此,我们的页面就会展现到浏览器中。\
更多推荐
所有评论(0)