前言
前面讲到了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
}
}
//JS中文网 – 前端进阶资源分享 www.javascriptc.com
此处的源代码其实还是很简单的,
- 调用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
}
//JS中文网 – 前端进阶资源分享 www.javascriptc.com
上面代码为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
js
const app = Vue.createApp({
data() {
return {
list: ['a', 'b', 'c', 'd']
}
}
});
app.mount('#demo')
//JS中文网 – 前端进阶资源分享 www.javascriptc.com
packages/vue/examples/classic/hello.html
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>
//JS中文网 – 前端进阶资源分享 www.javascriptc.com
我们在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
}
}
//JS中文网 – 前端进阶资源分享 www.javascriptc.com
上面是整理后的第一次渲染的`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函数接受的参数是一样的,还是上面的值,无变化,来看下源码:
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 {
// 其他逻辑
}
}
//JS中文网 – 前端进阶资源分享 www.javascriptc.com
根据参数可以知道会走到当前if逻辑,会先插入骨架;然后执行`mountChildren`,n2.children通过上面的subtree可以知道,值为一个数组,数组里面有1个元素,就是咱们要渲染的ul;
const 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
)
}
}
//JS中文网 – 前端进阶资源分享 www.javascriptc.com
可以看到将会对n2.children进行遍历,n2.children只有一个元素,是ul
+ + 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.5、由上面参数可知,ul的类型为ul,此时会走到processElement函数,processElement函数的参数和patch函数的参数是一样的,进行了透传,看下源代码:
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)
}
}
//JS中文网 – 前端进阶资源分享 www.javascriptc.com
JS中文网 – 前端进阶资源教程 www.javascriptC.com
一个致力于帮助开发者用代码改变世界为使命的平台,每天都可以在这里找到技术世界的头条内容
根据参数n1为null可以知晓,会走到mountElement的逻辑,参数不会发生改变。执行mountElement的过程中会检测ul的children,发现ul的children下面有值,则会调用mountChildren函数:
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
optimized || !!vnode.dynamicChildren
)
//JS中文网 – 前端进阶资源分享 www.javascriptc.com
此时vnode.children为由4个li组成的数组;el为ul,anchor为null,parentComponent为instance实例;parentSuspense为null;isSVG为false;optimized为true;重复上面mountChildren函数;
+ + 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部分进行讲解,慢慢把它们都消化掉。
作者:德莱问
链接:https://juejin.im/post/6883072260123394061
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文著作权归作者所有,如若转载,请注明出处
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com