系列文章目录
- 目录分析
- 初始化流程
- 响应式系统
- shared工具函数
文章目录
前言
vue3项目,需要通过createApp()
函数创建一个vue实例,本文将从该方法入口分析~
一、createApp在项目中的使用
在vue3中,每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的:
(取代了Vue2中 new Vue(options) 方式)
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
- 这里的
App
是根组件,作为渲染组件的起点 mount('#app')
表示要被挂载的DOM节点
二、createApp源码追溯
vue3支持多平台的,不同的平台createApp都有各自的定义,下面以常见的浏览器环境为例讲解~
如下(packages/runtime-dom/src/index.ts):
export const createApp = ((...args) => {
// 第一步:创建了app实例,ensureRenderer方法意义在于确保有渲染器
const app = ensureRenderer().createApp(...args)
// 第二步:重写了app.mount方法
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
...
}
//第三步:返回app实例
return app
})
这个入口API createApp方法,就三个步骤
- 第一步,创建app实例
- 第二步,重新app的mount方法
- 第三步,返回app
1.创建app实例
下面分析:const app = ensureRenderer().createApp(...args)
这行代码是如何实现的?
1.1 ensureRenderer
// packages/runtime-dom/src/index.ts
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
- 这个方法称之为“惰性 创建 renderer”——这里是为了只有执行createApp时,才给renderer渲染器赋值(
renderer = createRenderer(rendererOptions)
),也是优化的一点。 createRenderer
是在runtime-core
模块定义的,是【通用】【核心】的【创建渲染器】的方法。- 不同平台的具体渲染API是不一样的,根据传入的
rendererOptions
,就可以体会对vue3多平台的支持
看看rendererOptions具体传入的什么?
const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps);
patchProp
、forcePatchProp
传给runtime-core的render,用于更新DOM节点propnodeOps
主要是利用dom相关API封装了render渲染器需要的方法,传给runtime-core的render,用于更新DOM节点创建、插入、移除…等节点操作
// nodeOps
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
...
return el
},
createText: text => doc.createTextNode(text),
...
}
- nodeOps主要是利用
dom API
封装了render渲染器需要的方法,例如用DOMinsertBefore
封装了insert
方法,用removeChild
封装了remove
方法……
看看createRenderer<Node, Element>(rendererOptions)?
createRenderer
方法在runtime-core中实现,各平台通用的创建渲染器render方法
export function createRenderer< HostNode = RendererNode, HostElement = RendererElement>(options: RendererOptions<HostNode, HostElement>) {
// baseCreateRenderer是为跨平台设计的
return baseCreateRenderer<HostNode, HostElement>(options)
}
baseCreateRenderer
:方法实现基本是三个部分,如下
- 第一步:各平台传入的options中的API方法 重新统一命名
- 第二步:定义各种渲染需要的方法(patch,mountComponent,render…)
- 第三步:返回一个含有 {render, hydrate,
createApp
} 属性的对象
function baseCreateRenderer(
options: RendererOptions, //跨平台设计,不同平台传入不同的options
createHydrationFns?: typeof createHydrationFunctions
) {
// 第一步:各平台传入的options中的API方法 重新统一命名
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
...
} = options
// 第二步:定义各种渲染需要的方法
const patch = (n1, n2,container...)=>{...}
const render = (vnode, container, isSVG) => {...}
const mountComponent = (initialVNode, container...) => {...}
....
// 第三步:返回一个含有 {render, hydrate, createApp} 属性的对象
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
通过以上分析:
ensureRenderer()得到一个带有 {render, hydrate, createApp
} 渲染器属性的对象
1.2 ensureRenderer().createApp(…args)
由上可知,ensureRenderer()中返回的对象含有createApp: createAppAPI(render, hydrate)
方法,所以 ensureRenderer().createApp(…args)才可以正常调用,那继续分析:
createAppAPI(render, hydrate)
如下:
- 通过闭包返回一个
createApp
方法,并把 render 方法保留下来供内部来使用,这个createApp方法就是我们在项目中使用到的Vue.createApp(App)
。 - createApp内部定义一个app实例,包含_uid、_component、_props、_container、_context、version、config属性和use、mixin、component、directive、mount、unmount、provide全局方法
export function createAppAPI<HostElement>(
render: RootRenderFunction, // 在baseCreateRenderer中传入的render
hydrate?: RootHydrateFunction
){ // 闭包
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
// 创建了app实例,并返回
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) {},
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) {...}
})
return app
}
}
在应用项目中,实际打印app可得:
const app = Vue.createApp(MyComponent);
console.log('app', app);
与createApp
方法代码中定义的app是一致的:
那么通过以上分析,可得知const app = ensureRenderer().createApp(...args)
的实现原理。
2. 重写app.mount方法
2.1 createAppAPI 中的mount
在上面createAppAPI创建app实例方法中,定义了mount:
mount(rootContainer: HostElement,isHydrate?: boolean,isSVG?: boolean): any {
if (!isMounted) {
// 第一步:创建 root vnode
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
if (isHydrate && hydrate) { // 服务端渲染相关
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
}
isMounted = true
return vnode.component!.proxy
}
},
- 应用实例app的mount首次挂载
isMounted
为false - 首先通过
createVNode
(rootComponent,rootProps)来创建vnode节点 - 然后在非服务端渲染下通过
render
(cloneVNode(vnode), rootContainer, isSVG)渲染器将vnode节点转为真正的DOM节点,实现挂载 isMounted
最终置为true
2.2 createApp 中重定义的mount
export const createApp = ((...args) => {
...
// 缓存已有的mount方法
const { mount } = app
// 重写mount
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 处理containerOrSelector,获取真实的DOM元素
const container = normalizeContainer(containerOrSelector)
if (!container) return
// 获取定义的 Vue app 对象, 之前的 rootComponent
const component = app._component
// 如果不是函数、没有 render 方法、没有 template 使用 DOM 元素内的 innerHTML 作为内容
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// 挂载之前清空内容
container.innerHTML = ''
// 真正的挂载,调用上面缓存原定义的mount方法
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
}
- 首先缓存了之前定义好的mount挂载方法:
const { mount } = app
- 又重新定义了app.mount,其实是对之前的mount做了一层包装
- 处理
containerOrSelector
,如果传的是字符串选择器,通过 document.querySelector 方法得到与之对应的DOM元素。(通常我们应用中是这样使用的:mount('#app')
) - const proxy =
mount
(container, false, container instanceof SVGElement) 调用上面缓存的app实例的mount方法,mount其实也是执行了render
进行真正的渲染挂载。 - 去除
v-cloak
属性。v-cloak这个指令保持在元素上直到关联组件实例结束编译
通过以上分析,可知createApp(App).mount('#app')
的实现原理~
总结
我们从以上分析可以得出:
- creatApp API创造了一个app实例,创造过程中根据不同平台创造了渲染器render,并提供给内部使用。
- vue3支持跨平台渲染,核心创建渲染器
baseCreateRenderer
方法是抽离在runtime-core
中。 mount
过程实际就是执行了render