前言
前面讲到了Vue3中mount
里面做的事情,其中第一步的mount
前面已经讲解了,本篇文章将会从第二步的runtime-core文件夹下面的apiCreateApp文件mount函数开始讲起,依次讲解剩下的几步。按照主流程进行核心代码的解读,跳跃可能会比较大,不过在讲解过程中都会标明是哪个文件。
正文
正文从这开始,mount从此开始。 先来看下源码,前面讲到的dom部分的源码此处不多说,从core部分开始讲起:
packages/runtime-core/src/apiCreateApp.ts
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
vnode.appContext = context
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer)
}
isMounted = true
app._container = rootContainer
;(rootContainer as any).__vue_app__ = app
return vnode.component!.proxy
}
}
此处的源代码其实还是很简单的,
- 调用createVNode获取vnode,rootComponent即为调用createApp(config)的时候传递进来的config数据,rootProps为root props,前面提到过会对此进行校验,一般在使用过程中,rootProps为null;
- 保存context在跟节点上;
- 调用渲染函数,此处只讲解render;
- isMounted置为true;
- 实例的_container保存为当前rootContainer;
- rootContainer增加属性__vue_app__,置为当前app实例;
- 返回vnode.component的代理。
核心渲染代码为render函数。
render
render函数的作用在Vue2和Vue3中是完全不一样的,
- Vue2中render函数是做具体工作的,是真正的render操作,返回的结果是vnode,可以在这回顾下Vue2源码解读(七)-mount;
- Vue3中render函数是做分发工作的,相当于是一个路由器,两条线路,unmount和patch,无返回结果。
来看下render的源码:
packages/runtime-core/src/renderer.ts
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
}
上面代码为render函数的源码:
- 参数1:vnode,是要更新到页面上的vnode,通过上面createVNode获得;container为展现的容器;
- 先是对vnode进行了判断,如果为空,并且container._vnode有值,也就是有之前的dom渲染,则进行unmount操作;
- 如果vnode不为空,则进行patch操作,dom diff和渲染;
- 执行flushPostFlushCbs函数,回调调度器,使用Promise实现,与Vue2的区别是Vue2是宏任务或微任务来处理的
- 把container的_vnode存储为当前vnode,方便后面进行dom diff操作,此处和Vue2中是一样的。
因为是渲染,vnode不会为空,肯定会走到patch函数部分,来看下patch部分的代码:
packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
n1, // old
n2, // new
container, // 容器
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
// 如果type不相同,则把n1直接卸载掉
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const {type, ref, shapeFlag} = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
}
}
已上门patch函数为入口进行梳理分析,得到了下面的图,在其中有几条比较常用的线路:
- processFragment:处理片段(dom数组)的函数;
- processElement:处理元素的函数;
- processComponent:处理组件的函数; 接下来我们将会研究一个例子,会涉及到processFragment和processElement,做一个dom的diff操作;
render例子
我们现在将从头到尾开始讲解一个例子,将会从头到尾,一步步讲解用到的函数。假如现在有一个列表:
packages/vue/examples/classic/hello.js
const app = Vue.createApp({
data() {
return {
list: ['a', 'b', 'c', 'd']
}
}
});
app.mount('#demo')
packages/vue/examples/classic/hello.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,
user-scalable=no,target-densitydpi=medium-dpi,viewport-fit=cover"/>
<title>Vue3.js hello example</title>
<script src="../../dist/vue.global.js"></script>
</head>
<body>
<div id="demo">
<ul>
<li v-for="item in list" :key="item">
{{item}}
</li>
</ul>
</div>
<script src="./hello.js"></script>
</body>
</html>
我们在Vue3源代码的根目录,对应的目录下面新建hello.js和hello.html两个文件,把上面代码复制到对象文件中,然后到根目录运行npm run dev
,好了,现在项目跑起来了。然后打开浏览器,输入url地址:
file:///Users/draven/mywork/vue-3.0.0/packages/vue/examples/classic/hello.html
;可以看到页面的渲染:
进行到这里,就是成功的了,我们下一步研究页面上的效果是如何运行出来的,从上面的render函数说起。
- 1、开始运行:调用
render(vnode, rootContainer)
,该函数的运行位置位于packages/runtime-core/src/apiCreateApp.ts
,render函数的声明位于packages/runtime-core/src/renderer.ts
;参数vnode为上面调用createVNode所生成的,参数rootContainer就是我们上面传进来的id为demo的元素; - 2、接下来进入的是
packages/runtime-core/src/renderer.ts
文件,接下来的功能大部分都在这个文件里面,如有特殊情况会进行说明。 - 3、接下来运行:在render函数内部调用
patch(container._vnode || null, vnode, container)
-
- 3.1、第一个参数为老的vnode,因为是首次渲染,老的vnode是不存在的,所以为null;第二个参数就是透传的vnode;第三个参数为透传的container(#demo);
-
- 3.2、patch函数还接受其他参数,不过咱们暂时用不到:
patch(n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false)
;n1即为null,n2即为要更新的vnode,container为透传#demo;
- 3.2、patch函数还接受其他参数,不过咱们暂时用不到:
-
- 3.3、此时的n1为null,n2目前还是一个对象:
此时的判断会符合
shapeFlag & ShapeFlags.COMPONENT
,走到processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
函数;此时参数的值是:n1为null,n2如图所示,container为#demo;
- 3.3、此时的n1为null,n2目前还是一个对象:
此时的判断会符合
- 4、processComponent函数会进行对n1的判断,n1不为null,则证明是更新操作,调用updateComponent;此时,我们是首次渲染,所以不会走更新操作,走另外一个逻辑;如果为keepAlive的类型的组件,走activate逻辑;此时,我们不为keepalive的组件,所以走mountComponent函数,
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
,参数n2为上图,container为#demo,其他的参数目前还都是默认null(false)值; - 5、mountComponent函数会首先调用createComponentInstance生成对当前n2的实例,然后调用setupComponent初始化props和slots等,最终调用
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized )
,参数instance为上面生成的实例,initialVNode还是为上图n2,container为#demo,其他为默认值; - 6、setupRenderEffect函数是一个非常核心的函数,此函数将会为当前实例挂载上update方法,update方法是通过effect生成的,effect在Vue3中的作用就相当于Vue2中的observe;update生成后,挂载之前会先运行一下生成的effect方法,最后返回当前effect方法给update;运行effect函数就相当于Vue2中watcher调用get的过程.effect接受两个参数,第一个参数就是componentEffect函数,也就是监听变化调用此函数;上面讲到先运行一下生成的effect方法,生成的effect方法内部就会调用这个componentEffect函数;
- 7、componentEffect函数有两个逻辑,判断是否已经渲染:instance.isMounted;如果已经渲染,则走更新逻辑;咱们还未渲染,则走未渲染的逻辑;来看下这部分的源码。
function componentEffect() { if (!instance.isMounted) { let vnodeHook: VNodeHook | null | undefined const {el, props} = initialVNode const {bm, m, parent} = instance // beforeMount hook if (bm) { invokeArrayFns(bm) } // onVnodeBeforeMount if ((vnodeHook = props && props.onVnodeBeforeMount)) { invokeVNodeHook(vnodeHook, parent, initialVNode) } const subTree = (instance.subTree = renderComponentRoot(instance)) if (el && hydrateNode) { hydrateNode( initialVNode.el as Node, subTree, instance, parentSuspense ) } else { patch( null, subTree, container, anchor, instance, parentSuspense, isSVG ) initialVNode.el = subTree.el } if (m) { queuePostRenderEffect(m, parentSuspense) } if ((vnodeHook = props && props.onVnodeMounted)) { queuePostRenderEffect(() => { invokeVNodeHook(vnodeHook!, parent, initialVNode) }, parentSuspense) } const {a} = instance if ( a && initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ) { queuePostRenderEffect(a, parentSuspense) } instance.isMounted = true } else { // no first render } }
上面是整理后的第一次渲染的componentEffect
函数源码;
-
- 7.1、先调用了当前实例的beforeMount钩子函数;
-
- 7.2、调用n2的父类的BeforeMount钩子函数;
-
- 7.3、调用renderComponentRoot函数进行渲染组件的根元素;
-
- 7.4、调用patch:
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
;此时subtree的值为:;container为#demo;anchor为null,instance为当前实例,parentSuspense为null,isSVG为false;
- 7.4、调用patch:
-
- 7.5、调用当前实例的mounted钩子函数;调用n2的父类的mounted钩子函数;调用当前实例的activated钩子函数;不是直接调用,而是通过queuePostRenderEffect放到队列中去调用;
-
- 7.6、最终把实例的isMounted置为true;
- 8、上面componentEffect函数中调用patch才是正式渲染的开始,前面大部分都是相当于数据的整理:
-
- 8.1、按照上面componentEffect函数的运行参数传递到patch函数:
patch(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
;此时n1为null,n2为上图(subtree),container还是#demo,anchor为null,parentComponent为上面的instance实例,parentSuspense为null,isSVG为false,optimized为false;
- 8.1、按照上面componentEffect函数的运行参数传递到patch函数:
-
- 8.2、代码依次执行,通过上图可以看到,component获取到的实例的subtree的type为Fragment,则会走到processFragment函数;
-
- 8.3、processFragment接受的参数和patch函数接受的参数是一样的,还是上面的值,无变化,来看下源码:
根据参数可以知道会走到当前if逻辑,会先插入骨架;然后执行const processFragment = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) => { const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))! const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))! let {patchFlag, dynamicChildren} = n2 if (patchFlag > 0) { optimized = true } if (n1 == null) { hostInsert(fragmentStartAnchor, container, anchor) hostInsert(fragmentEndAnchor, container, anchor) mountChildren( n2.children as VNodeArrayChildren, container, fragmentEndAnchor, parentComponent, parentSuspense, isSVG, optimized ) } else { // 其他逻辑 } }
mountChildren
,n2.children通过上面的subtree可以知道,值为一个数组,数组里面有1个元素,就是咱们要渲染的ul;
可以看到将会对n2.children进行遍历,n2.children只有一个元素,是ulconst mountChildren: MountChildrenFn = ( children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0 ) => { for (let i = start; i < children.length; i++) { const child = (children[i] = optimized ? cloneIfMounted(children[i] as VNode) : normalizeVNode(children[i])) patch( null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } }
- 8.3、processFragment接受的参数和patch函数接受的参数是一样的,还是上面的值,无变化,来看下源码:
-
- 8.4、使用上面的运行时的参数,调用
patch(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG = false, optimized)
;参数:n1为null;child为上面提到的ul;container为#demo,anchor为上面processFragment函数里面的fragmentEndAnchor;parentComponent为instance实例;parentSuspense为null;isSVG为false;optimized为true,因为在上面processFragment里面进行了改变;
- 8.4、使用上面的运行时的参数,调用
-
- 8.5、由上面参数可知,ul的类型为ul,此时会走到processElement函数,processElement函数的参数和patch函数的参数是一样的,进行了透传,看下源代码:
根据参数n1为null可以知晓,会走到mountElement的逻辑,参数不会发生改变。执行mountElement的过程中会检测ul的children,发现ul的children下面有值,则会调用mountChildren函数:const processElement = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) => { isSVG = isSVG || (n2.type as string) === 'svg' if (n1 == null) { mountElement( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else { patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized) } }
此时vnode.children为由4个li组成的数组;el为ul,anchor为null,parentComponent为instance实例;parentSuspense为null;isSVG为false;optimized为true;重复上面mountChildren函数;mountChildren( vnode.children as VNodeArrayChildren, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren )
- 8.5、由上面参数可知,ul的类型为ul,此时会走到processElement函数,processElement函数的参数和patch函数的参数是一样的,进行了透传,看下源代码:
-
- 8.6、mountChildren函数里面进行for循环的时候,li的type为li,则会继续走到processElement,重复上面步骤,依次执行完成;
- 9、上面所有的步骤执行完成,现在数据已经呈现到页面上了。
- 10、此时基本所有的事情都干完了,也就是相当于主队列空闲了,调用
flushPostFlushCbs()
开始执行队列里面的函数; - 11、最后把container的_vnode属性指向当前vnode;方便下次做dom diff使用。
- 12、第一次渲染运行完成。
结语
本章着重降了first render的渲染过程,下一章会像按照本章的节奏结合Vue2源码解读(七)中的dom diff部分对Vue3中的patch部分进行讲解,慢慢把它们都消化掉。