我们先从一个最简单的例子来分析渲染流程:
<html>
<head>
<style type="text/css">
</style>
</head>
<body>
<div id="app">
<div>hello {{state.msg}}</div>
</div>
<script src="../dist/vue.global.js"></script>
<script>
const {createApp, reactive, onBeforeMount, onBeforeUpdate, onUpdated, onMounted, onRenderTracked, onRenderTriggered} = Vue
const app = createApp({
setup(){
const state = reactive({
msg: 'Vue3.0'
})
return {state}
}
}).mount('#app')
</script>
</body>
</html>
上面例子代码输出一行字符hello Vue3.0,我们看看怎么一步一步渲染到页面的。
1. 从createApp函数开始,看一下这个函数的实现:
export const createApp = ((...args) => {
//1. 创建app对象
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
}
const { mount } = app
//2. 重写app对象的mount方法,重写这个方法的原因主要是为了跨平台。
// app对象的mount方法是一个标准的跨平台渲染方法(创建VNode,渲染VNode)
// 在Web平台下渲染初的VNode挂载到Dom对象上,而在小程序其他平台上可能要挂载到其他对象上
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)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
}) as CreateAppFunction<Element>
这个函数比较短小,主要做两件事,第一件是创建app对象,第二件是重写app对象的mount方法,至于为什么要重写mount方法,已在注释中说明。最后返回这个app对象。
2. 接下来继续看创建app对象过程。只有一行代码const app = ensureRenderer().createApp(...args), ensureRenderer函数是用来创建一个渲染器,然后调用这个渲染器的createApp方法。ensureRenderer 创建渲染器方法也是为了跨平台考虑的,Web平台有自己的渲染器,小程序其他平台可能要用其他渲染器。类似插件的形式,各个渲染器最后都要提供createApp方法。ensureRenderer函数也比较简单:
function ensureRenderer() {
console.log(`liubbc ensureRenderer 01`)
//调用createRenderer方法创建渲染器,参数是renderOptions,可以根据不同的平台传递不同的参数
//Web平台就是document.createElement, document.createTextNode等方法
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)
//这里就是renderOptions参数(在Web平台下提供的方法,在其他平台下就需要提供另外的方法了)
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
//.....
createElement: (tag, isSVG, is): Element =>
isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined),
createText: text => doc.createTextNode(text),
}
这里我们给创建渲染器函数createRenderer函数提供的参数是Web平台下的。
3. 接下来我们看看,createRenderer创建通用渲染器流程。
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
console.log(`liubbc createRenderer 01`)
//调用baseCreateRenderer函数创建渲染器
return baseCreateRenderer<HostNode, HostElement>(options)
}
// implementation
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// compile-time feature flags check
if (__ESM_BUNDLER__ && !__TEST__) {
initFeatureFlags()
}
//先把传递过来的Web平台下的渲染器方法解构出来
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
//....
//定义的render函数,最后要把这个函数给export出去,这个函数有两个参数,vnode就是要渲染的
//虚拟node,container就是要挂载的地方
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
console.log(`liubbc patch 02`)
//patch方法用来首次渲染,或非首次diff更新
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
//...
//最后返回一个对象,这个对象提供了createApp函数,而createApp函数是createAppAPI函数返回值
//而且createAppAPI函数的参数是render函数
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
//createAppAPI方法只是一个包装函数,直接返回一个createApp函数
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对象,只是一个Object对象而已,这个对象提供了一些方法,就包括mount方法
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
//app对象提供的mount方法,也只有调用这个方法之后才进行开始渲染,所以我们也可以认为这个
//函数是渲染起点
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
//在这里创建虚拟Node节点
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)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
//在这里调用createAppAPI传递进来的参数render方法,开始进行对VNode的渲染
render(vnode, rootContainer)
}
isMounted = true
//...
}
//最后要返回这个app对象
return app
}
上面的创建渲染器过程还是比较绕的,还是主要为了跨平台考虑的。采用的方法,主要是函数作为参数,以及返回值为函数这种函数式编程思想。到这来创建渲染器过程就分析完了。
4. 最后再看看调用mount方法的地方,也就是文章刚开始我们最简单例子调用的地方。
const app = createApp({
setup(){
const state = reactive({
msg: 'Vue3.0'
})
return {state}
}
}).mount('#app')
我们再把这个流程串联起来:
调用createApp返回app对象 ------> 调用app对象重写的mount方法 -----> 对要挂载的对象标准化,然后调用app对象提供的mount方法 ------> 创建VNode,初次渲染或非初次diff更新