现如今前端的三大框架Vue、React、nodeJS是从事前端开发的必备技能,2020年9月随着Vue3的发布,Vue也迎来了更多开发者的目光,学习源码也要提上日程。这篇专栏将详细解析Vue3源码的主要流程以及用法的底层实现。
创建一个Vue实例
const Vue = createApp(/* options */).mount('#app')
可以看到Vue3的入口函数是调用了createApp
这个方法,它是Vue3对外暴露的一个函数,一起来看下它的内部实现
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
// ...
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {}
return app
}) as CreateAppFunction<Element>
createApp
函数首先生成一个app
对象,再将app
中的mount
方法解构出来,重新定义app
的mount
方法,后续我们使用mount
挂载时再详细解析,最后返回app对象。接下来先看下app
对象是如何生成的。
app对象
const app = ensureRenderer().createApp(...args)
首先我们调用ensureRenderer()
方法
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// ...
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
forcePatchProp: hostForcePatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent
} = options
// ...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
可以看到ensureRenderer
方法最终执行了baseCreateRenderer
方法,创建了options
对象,该对象中定义的一些属性是DOM
操作的方法,例如节点的增加、删除、移位等等,后续解析VNode
对象时会调用这些方法生成真正的DOM
。方法最终返回了一个对象,当我们调用ensureRenderer
返回的createApp
属性时,就是调用了createAppAPI
方法的返回值
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
// ...
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
const app: App = (context.app = {
// ...
_component: rootComponent as ConcreteComponent,
//...
})
return app
}
}
可以看到createAppAPI
方法返回了一个createApp
函数,所以最终是调用了createApp
方法,此方法创建了一个app
对象,定义了一些属性方法,例如mount
、unmount
等等,最终将这个对象返回。
mount挂载
当app
对象生成之后,回看之前我们创建一个Vue实例时,在调用createApp
方法之后会执行mount
方法,也就是app.mount
方法。接下来我们看下新的app.mount
方法具体做了什么
// normalizeContainer 方法
function normalizeContainer(
container: Element | ShadowRoot | string
): Element | null {
if (isString(container)) {
const res = document.querySelector(container)
// ...
return res
}
// ...
return container as any
}
// 重新定义的app.mount方法
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) {
component.template = container.innerHTML
// ...
}
// 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
}
该方法首先调用normalizeContainer函数,通过document.querySelector('#app')
找到id
为app
的DOM
节点(后续称之为root
节点)。然后获取app
的_component
对象,其实就是调用createApp
方法时传入的options
参数对象(包含data、methods、computed
属性等等)。
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
判断传入的options
参数不是函数类型、没有render
属性 和 template
属性,则root
节点的内部HTML
字符串赋值给options
参数的template
属性
container.innerHTML = ''
并将内部HTML
字符串置为空。那现在页面上只有root
节点,其内部的子节点都清空了,后续会将template
字符串进行模版编译,重新添加子节点。
// container即为root节点
const proxy = mount(container, false, container instanceof SVGElement)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
最后执行mount
方法(老的app.mount
方法,是在createAppAPI
方法的返回结果函数中定义的),root
节点移除的v-cloak
属性,新增data-v-app
属性。
总结
从Vue3
创建实例的方法开始,找寻它的入口函数。利用函数柯里化,先执行ensureRenderer
方法,定义一些操作DOM
节点所需要的函数、将虚拟DOM
对象解析为真实DOM
的方法等等。紧接着执行createApp
方法,该方法定义了一个app
对象,创建了一些属性,例如_component
就是保存传入的options
参数,定义了一些方法,例如mount、unmount
等等,到此app
对象就形成了。后续执行.mount('#app')
方法,其实就是执行定义在app
上的mount
方法进行挂载。这里需要注意的是,原mount
方法解构出来之后,定义了新的app.mount
方法,在新方法中主要是找寻root
根节点,将根节点内部的HTML
字符串保存在template属性上(为后续的模版编译做准备),然后清空root
节点内部,最后执行原mount
方法进行挂载操作