先看一下vue-next官方文档的介绍:
每个 Vue 应用都是通过用
createApp
函数创建一个新的应用实例开始的
传递给
createApp
的选项用于配置根组件。当我们挂载应用时,该组件被用作渲染的起点。
一个应用需要被挂载到一个 DOM 元素中。例如,如果我们想把一个 Vue 应用挂载到
,我们应该传递
#app
我们将分为两部分进行渲染过程的理解:
- 创建应用实例,函数
createApp
的剖析 - 应用实例挂载, 函数
mount
方法挂载过程
本篇详细讲述调用方法createApp
过程
创建应用实例 createApp
下面是一个简单的demo
双向绑定:{{value}}
const { createApp } = Vue
const helloComp = {
name: 'hello-comp',
props: {
personName: {
type: String,
default: 'wangcong'
}
},
template: 'hello {{personName}}!
'
}
const app = {
data() {
return {
value: '',
info: {
name: 'tom',
age: 18
}
}
},
components: {
'hello-comp': helloComp
},
mounted() {
console.log(this.value, this.info)
},
}
createApp(app).mount('#app')
现在我们从createApp函数为入口,去了解应用创建的过程。
查看官方文档和上面的例子我们可以知道,createApp
方法接收的是根组件对象作为参数,并返回了一个有mount
方法的应用实例对象。
按照依赖关系可以找到createApp
方法出自packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
}
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
return proxy
}
return app
}) as CreateAppFunction
这里做了两件事情:
- 创建
app
应用实例:ensureRenderer().createApp(...args)
- 重写了
app.mount
方法。document.querySelector
方法获取HTMLElement
对象作为参数传入原mount
方法。该部分会在mount
段落详细讲解。
ensureRenderer
ensureRenderer
函数的目的是惰性创建renderer
对象,这样做目的是在用户只引入reactivity
模块时,对renderer核心逻辑部分可以进行tree-shake。
renderer
对象包含三个属性:
render
方法、 hydrate
( ssr客户端激活相关)、createApp
方法
renderer
对象实际上是方法createRenderer
函数返回的。
// nodeOps: dom节点增删改查操作的原生api
const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)
let renderer: Renderer | HydrationRenderer
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
createRenderer
这个方法在packages/runtime-core/src/renderer.ts
中定义。
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions) {
return baseCreateRenderer(options)
}
createRenderer
方法接受两个通用的类型参数HostNode
和HostElement
。其目的是在自定义渲染器中可以传入特定于平台的类型;
例如:
对于浏览器环境runtime-dom
,HostNode
将是DOM Node
接口;HostElement
将是DOM Element
接口。
Element继承了Node类,也就是说Element是Node多种类型中的一种,即当NodeType为1时Node即为ElementNode,另外Element扩展了Node,Element拥有id、class、children等属性。
baseCreateRenderer
这个方法也在packages/runtime-core/src/renderer.ts
中定义
该方法比较长,暂时忽略中间代码。该函数执行返回了一个对象(上文中的renderer
对象):
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
-
render
:接受两个参数VNode
,Element
;
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
-
hydrate
:与ssr客户端激活相关 -
createApp
:接收方法createAppAPI(render, hydrate)
的返回值
createAppAPI
这个方法在packages/runtime-core/src/apiCreateApp.ts
中定义。
在这里createAppAPI
的返回结果是一个函数createApp
,这里终于找到了demo中调用的那个接受跟组件对象的createApp
函数。
createApp
返回了应用实例app
对象,其中包含了我们比较熟悉的一些方法,例如:mixin
、component
、directive
等;
export function createAppAPI(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction {
return function createApp(rootComponent, rootProps = null) {
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()
let isMounted = false
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
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[]) {
...
return app
},
mixin(mixin: ComponentOptions) {
...
return app
},
// 组件注册
component(name: string, component?: Component): any {
...
return app
},
// 指令注册
directive(name: string, directive?: Directive) {
...
return app
},
// dom挂载
mount(rootContainer: HostElement, isHydrate?: boolean): any {
...
return app
},
// 卸载
unmount() {
if (isMounted) {
render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsUnmountApp(app)
}
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
},
// 注入
provide(key, value) {
...
return app
}
}
return app
}
}
下面就是在应用实例app
还没有调用mount
方法进行挂载前的属性:
这里强调一下app._component
引用的就是我们传入createApp
的根组件对象
对比
2.x global API:
import Vue from 'vue'
import App from './App.vue'
Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)
Vue.prototype.customProperty = () => {}
new Vue({
render: h => h(App)
}).$mount('#app')
从技术上讲,Vue 2没有“应用”的概念。我们定义为应用的只是通过创建的根Vue实例new Vue()
。从同一Vue构造函数创建的每个根实例都共享相同的全局配置。
Vue当前的某些全局API和配置会永久更改全局状态。这会导致一些问题:
- 全局配置更容易使测试过程中意外污染其他测试案例
- 影响每一个根实例
Vue.mixin({ /* ... */ })
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })
vue3 应用程序实例
createApp
返回一个提供应用上下文的应用实例。应用实例挂载的整个组件树共享同一个上下文。该上下文提供了先前在Vue 2.x中“全局”的配置。该实例不会被应用于其他实例的任何全局配置所污染。共享实例属性应附加到应用程序实例的config.globalProperties
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)
app.provide(/* ... */)
app.config.globalProperties.customProperty = () => {}
app.mount(App, '#app')
完