前言
这是vue3系列源码的第三章,使用的vue3版本是3.2.45
。
背景
在前两篇文章里面,我们简单看了一下createApp
和mount
的主流程。对整体的调用流程有了一个大概的了解。
现在我们来思考两个问题,页面上,到底是从什么时候开始出现内容的,又是从什么时候开始加载内部元素的。
带着这两个问题,我们详细去了解一下上篇mount
中提到的一些函数。
前置
为了更简单明了的了解渲染的流程和看见效果,我们需要对我们的项目做一些前置工作。 我们在App.vue
文件里面,把script
都去掉,只留下一个template
元素,里面放上简单的元素。 同时,我们给容器标签#app
加一点样式,看的更直观一点。
js
<template>
<div>111</div>
</template>
<style>
#app {
color: #2c3e50;
width: 100px;
height: 100px;
background-color: pink;
}
</style>
#app
这里我们发现,在我们执行createApp之前,#app
这个元素就已经渲染出来了。
不过此时显示的只是一个空的容器,我们写的组件并没有被渲染出来。
所以接下来让我们去看看,组件及其内容到底是什么时候渲染在页面上的。
setupRenderEffect
这里我们接着上一篇mount
的流程,我们直接到setupRenderEffect
这个函数中。
它是定义在baseCreateRender
函数中的,老规矩,我们先看一下传进来的参数。
- instance,组件实例
- initialVNode,组件的vnode
- container, #app的DOM对象
js
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {...}
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope // track it in component's effect scope
))
const update: SchedulerJob = (instance.update = () => effect.run())
update.id = instance.uid
update()
}
这个函数先定义了一个函数componentUpdateFn
,这个函数后面作为参数传进了new ReactiveEffect
中, 最后执行的updata
执行就是执行new ReactiveEffect
得到对象的run
方法。
那我们先看一下这个ReactiveEffect
ReactiveEffect
js
class ReactiveEffect {
constructor(fn, scheduler = null, scope) {
this.fn = fn;
this.scheduler = scheduler;
this.active = true;
this.deps = [];
this.parent = undefined;
recordEffectScope(this, scope);
}
run() {
let parent = activeEffect;
let lastShouldTrack = shouldTrack;
while (parent) {
if (parent === this) {
return;
}
parent = parent.parent;
}
try {
this.parent = activeEffect;
activeEffect = this;
shouldTrack = true;
trackOpBit = 1 << ++effectTrackDepth;
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this);
}
else {
cleanupEffect(this);
}
return this.fn();
}
}
}
ReactiveEffect
类是定义在reactivity部分。 先看一下参数:
- fn,就是我们定义的
componentUpdateFn
函数 - scheduler,
() => queueJob(update)
- scope, 见下图
这里activeEffect是undefined,所以直接跳过while循环,进入try代码端。
try代码端里的核心就是return this.fn()
, 而这个fn就是之前定义的componentUpdateFn
函数
componentUpdateFn
js
const componentUpdateFn = () => {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
toggleRecurse(instance, false)
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
if (
!isAsyncWrapperVNode &&
(vnodeHook = props && props.onVnodeBeforeMount)
) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}
toggleRecurse(instance, true)
if (el && hydrateNode) {
...
} else {
const subTree = (instance.subTree = renderComponentRoot(instance))
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
initialVNode.el = subTree.el
}
...
} else {
...
}
}
这里的核心代码是
- renderComponentRoot,它其实就是把app组件里内容的vnode返回了出来,所以这里的
subTree
是vnode - patch, 把得到的subTree穿进去,那么其实就是处理app组件内容了。
我们先看一下renderComponentRoot
函数
renderComponentRoot
js
function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const { type, vnode, render, renderCache, data, ctx ... } = instance
let result
let fallthroughAttrs
const prev = setCurrentRenderingInstance(instance)
try {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block.
const proxyToUse = withProxy || proxy
result = normalizeVNode(
render!.call(proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx)
)
fallthroughAttrs = attrs
} else {
...
} catch (err) {
...
}
// attr merging
// in dev mode, comments are preserved, and it's possible for a template
// to have comments along side the root element which makes it a fragment
let root = result
let setRoot: SetRootFn = undefined
if (fallthroughAttrs && inheritAttrs !== false) {
const keys = Object.keys(fallthroughAttrs)
const { shapeFlag } = root
if (keys.length) {
if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)) {
...
root = cloneVNode(root, fallthroughAttrs)
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
...
}
}
}
}
result = root
setCurrentRenderingInstance(prev)
return result
}
先看一参数:
- instance, 是
App.vue
组件的实例
这段的主要工作是:
- 通过
setCurrentRenderingInstance
获取已经存在的渲染实例,然后将传入的组件实例保存在currentRenderingInstance
变量中,这里获取的是null,所以pre
也是null - 通过
normalizeVNode
获取App.vue
渲染内容的vnode - 通过
setCurrentRenderingInstance
把currentRenderingInstance
置为pre
的值也就是null,然后把上次保存的值也就是app组件的实例返回出来。
这里面我们简单看一下获取子vnode的部分
js
result = normalizeVNode( render!.call(proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx) )
render.call()
其实就是执行了render渲染函数,那么我们看一下最终得到什么,也就是传入normalizeVNode
的值
render.call()
最终得到的就是App
组件里内容的vnode,normalizeVnode
函数最终返回的值其实也就是这个vnode。
总结一下,renderComponentRoot
函数就是返回了内容的vnode。
patch
patch
函数我们在前一篇mount
文章里面说过。 我们这里面的vnode其实就是这一段内容,
js
<div>111</div>
就是一个div标签,所以在patch里面走到了ELEMENT里面。
js
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
那我们就看看processElement这个函数有什么不一样。
processElement
js
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {...}
}
一样的套娃,再看看mountElement
mountElement
到了这个函数,其实离我们内容的渲染已经很近了。
js
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, dirs } = vnode
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
// mount children first, since some props may rely on child content
// being already rendered, e.g. `<select value>`
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
hostInsert(el, container, anchor)
}
我们先看一下传入的参数:
- vnode, div的vnode
- container, #app DOM元素
- parentComponent, App组件的实例
这个函数的重点流程在:
- hostCreateElement,根据传入的vnode,创建实际的div DOM元素
- hostSetElementText,把vnode.children也就是111,塞到div的innerText中
- hostInsert,这个就是本次显示的最终环节了,这个函数一调用,可以看见页面上app元素上就挂在了这个div元素
这里的shapeFlag是9,ShapeFlags.TEXT_CHILDREN是8,8 & 9 等于8,是true
hostInsert
函数其实就是把div插入到app元素中。
js
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
}
至此,我们就借用一个简单的demo,搞清楚了vue3加载显示的流程了。
流程图
延伸
上面我们用了比较简单的页面结构来认识这个流程。
但是实际工作中的页面肯定比这复杂的多,那么我们这里稍稍拓展一点,加一点复杂度,加上子组件,来看看流程上会有什么不同。
这里我们再次改造一下App.vue
文件。
js
<template>
<div>111</div>
<HelloWorld msg="你好" />
</template>
<script setup>
import HelloWorld from './components/HelloWorld.vue';
</script>
<style>
#app {
color: #2c3e50;
width: 100px;
height: 100px;
background-color: pink;
}
</style>
HelloWorld.vue
文件
js
<template>
<div class="hello">
{{ msg }}
</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({msg: String})
</script>
最终显示的效果:
下面我们就再走一遍流程,看看哪里不一样,这里我只提一下不一样的地方。
processFragment
首先第一个不一样的地方。
在patch函数里面,执行的不再是processElement
了,而是processFragment
,因为此时的type是Symbol(Fragment)
js
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
我们看一下这个函数:
js
processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
if (n1 == null) {
// a fragment can only have array children
// since they are either generated by the compiler, or implicitly created
// from arrays.
mountChildren(
n2.children as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
...
}
}
看一下参数:
- n1, null
- n2, app组件内容代码段的vnode
- container, #app DOM对象
- parentComponent,app组件实例
这个函数最终的核心是mountChildren
函数
mountChildren
js
mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
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,
slotScopeIds,
optimized
)
}
}
看一下参数:
- children,见下图,是div元素和子组件
- container, app DOM对象
- parentComponent,app组件实例
这个函数里面干的事情其实很好理解,就是循环把子元素和子组件都丢进patch函数。
那么后面的事情大家应该都熟悉,无论是对组件的patch,还是对元素的patch过程,我们在之前的文章(mount)和上面都提到了。
核心就是这么一个逻辑:
- fragment的patch最终会向下一级进行,变为component的patch和element的patch。
- component的patch最终会向下一级进行,变为element的patch。
- element的patch中就会把元素添加到页面上了。
结尾
那么以上就是页面渲染部分的全部内容了。
从下一篇开始,我们将去探索页面的更新以及各种钩子和副作用的执行。
彻底搞明白vue3!