vue首次挂载流程

从vue.createApp开始

从vue.createApp开始,在runtime-dom/index.ts中的createApp方法,具体实现在runtime-core\src\renderer.ts的baseCreateRenderer

baseCreateRenderer返回create函数,值为createAppAPI返回值。

createAppAPI返回的值就是app实例,包含mixin、use、mount等方法。

参数归一化

背景:app.mout(‘#app’),这个函数是可以接受字符串和dom元素作为容器,是如何处理字符串的呢?

runtime-dom/index.ts中的createApp:

通过解构上述createAppAPI返回app对象获取到mount方法,重新将参数做处理,当为字符串时:

1
2
3
4
5
6
7
8
9
if (isString(container)) {
const res = document.querySelector(container)
if (__DEV__ && !res) {
warn(
`Failed to mount app: mount target selector "${container}" returned null.`,
)
}
return res
}

模板归一化

背景:编写vue模板代码有两种方式

当没有reder方法和template对象时,vue会自动添加一个template对象,值为模板内容:

1
2
3
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}

instance实例

在mountComponent中,通过createComponentInstance方法创建一个空的实例,然后在setupComponent中添加属性,具体添加过程是在setupStatefulComponent和finishComponentSetup添加的

compile编译(获得render方法)

在finishComponentSetup方法中,通过compile方法编译template获得render方法

1
2
3
Component.render = compile(template, finalCompilerOptions)

instance.render = (Component.render || NOOP) as InternalRenderFunction

packages\compiler-core\src\compile.ts:

这里就是具体实现了

1
const ast = isString(source) ? baseParse(source, resolvedOptions) : source

通过baseParse函数将template转换为ast抽象语法树,再进行转换。为什么要转换呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
transform(
ast,
extend({}, resolvedOptions, {
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {}, // user transforms
),
}),
)

这里就会有一个优化了,叫做静态提升,指的是 Vue 编译器识别模板中的静态内容(不依赖动态数据的部分),并将这些内容”提升”到渲染函数之外。这样这些静态内容只会被创建一次,而不是在每次组件重新渲染时都重新创建。

subTree

通过执行render函数获取到VNode

1
const subTree = (*instance*.subTree = renderComponentRoot(*instance*))

patch

将subTree通过patch挂载到container上。

1
2
3
4
5
6
7
8
9
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
namespace,
)

patch主要的参数:第一个是旧VNode,第二个是新VNode

挂载

通过调用hostCreateElement创建DOM,再通过hostInsert将创建的元素插入到container

更新DOM(diff算法)

无key更新(patchUnkeyedChildren)

函数会接受两个新旧VNode数组,获取到新旧VNode的长度,取较小值开始循环。每次循环将从第一个新旧VNode开始patch。循环结束后,比较新旧VNode的长度,oldLength > newLength则调用unmountChildren删除不需要渲染的节点;反之,则添加mountChildren添加新DOM

有key更新(patchKeyedChildren)

注意五个变量:

  • i:下标,从0开始
  • e1:旧VNode最后一个元素下标
  • e2:新VNode最后一个元素下标
  • s1:旧VNode中未匹配部分的起始索引,s1 = i
  • s2:新VNode中未匹配部分的起始索引,s2 = i

比较节点(isSameVNodeType:type和key相同,返回true),有以下几种情况:

  1. 从新旧VNode的下标i开始比较,相同则patch,i++,继续循环;不同则break

  2. 从新旧VNode的最后一个元素开始比较,相同则patch,e1–,e2–,继续循环;不同则break

  3. i > e1 && i <=e2,那么从i到e2的节点都是新增,循环patch

  4. i > e2 && i <=e1,那么从i到e1的节点都是删除的,循环unmount

  5. 复杂情况:

    // [i … e1 + 1]: a b [c d e] f g

    // [i … e2 + 1]: a b [e d c h] f g

    // i = 2, e1 = 4, e2 = 5

    // s1 = 2, s2 = 2

    1. e2与s1对比,若它们的key和type都相同,把s1对应的真实节点移动到e1对应的真实节点的后面,并且s1变成undefined。s1++,e2–
    2. e1与s2对比,若它们的key和type都相同,把e1对应的真实节点移动到s1对应的真实节点的前面,并且e1变成undefined。s2++,e1–