Vue3 原理篇之 Block Tree

前言

安装 npm install @vue/cli -g
构建项目 vue create vue3-compiler

用vite安装vue3项目

1
2
3
4
5
npm i -g create-vite-app
create-vite-app demo3
cd demo3
npm instal
npm run dev

vue3一样需要把我们写的template最终变成render方法,看一下vue3的转换器

编译过程

  • 先将模板进行分析 生成对应的ast树(对象来描述语法);
  • 做转化流程 transform -> 对动态节点做一些标记 指令、插槽、事件、属性… patchFlag
  • 代码生成 codegen -> 生成最终的代码

Block的概念 -> Block Tree

  • diff算法的特点是递归遍历,每次比较同一层,比完自己比儿子、孙子…性能比较差,不停的递归,之前写的都是全量比对,所有的属性、方法都要比对。
  • block的作用就是收集动态节点(它下面所有的);将树的递归拍平成了一个数组,以前要diff就要diff【children】 children下的children,现在只要diff【dynamicChildren】
  • createVNode的时候就会判断这个节点是否是动态的,是的话就让外层的block收集起来
  • 目的是为了diff的时候只diff动态的节点

举例:

1】

1
2
<div></div>
<div></div>

vue3里面可以有多个跟节点,编译会生成_Fragment包裹;

2】

添加动态节点

1
2
<div>{{name}}</div>
<p><span><a>{{age}}</a></span></p>

编译后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("div", null, _toDisplayString(_ctx.name), 1 /* TEXT */),
_createVNode("p", null, [
_createVNode("span", null, [
_createVNode("a", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
])
])
], 64 /* STABLE_FRAGMENT */))
}

console.log(render({name: 'jf', age: 18}));
// Check the console for the AST

生成的ast树:

block的作用就是收集动态节点

3】

1
2
3
4
5
6
7
8
9
10
<div>
<template v-if="flag">
<p>hello word</p>
<span>{{title}}</span>
</template>
<template v-else>
<span>{{title}}</span>
<p>hello word</p>
</template>
</div>
1
2
3
4
block -> div 父亲更新 会找到dynamicChildren => 子的block和动态节点
block(v-if key="0") <div>xxx</div>
block -> div
block(v-if key="1") <div>xxx</div>

编译后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createVNode as _createVNode, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock, createCommentVNode as _createCommentVNode } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
(_ctx.flag)
? (_openBlock(), _createBlock(_Fragment, { key: 0 }, [
_createVNode("p", null, "hello word"),
_createVNode("span", null, _toDisplayString(_ctx.title), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
: (_openBlock(), _createBlock(_Fragment, { key: 1 }, [
_createVNode("span", null, _toDisplayString(_ctx.title), 1 /* TEXT */),
_createVNode("p", null, "hello word")
], 64 /* STABLE_FRAGMENT */))
]))
}

console.log(render({flag: true}));
// console.log(render({flag: false}));

可以看到key从 0->1 需要全量比对

如果会影响dom结构的,都会被标记成block节点: v-if v-else
动态内容自己和父节点都会标记为block,父亲会收集儿子的block -> Block Tree;

4】

1
2
3
<div>
<span v-for="item in count">{{item}}</span>
</div>
1
2
3
4
5
block -> div
block -> v-for 不收集动态节点了 2个节点

block -> div
block -> v-for 3个节点

v-for 序列不稳定,改变结构的也要封装到block中,期望的更新方式是拿以前的和现在的做对比,靶向更新,如果前后节点不一致,只能全量比对
两个儿子(children)的全量比对,全量diff就是用之前的递归方法去比

Block Tree做的就是保证需要更新的时候能知道哪些节点需要更新

patchFlag

描述不同的动态节点,以此来做相对应的比对

源码:packages/shared/src/patchFlags

render方法
源码:packages/runtime-core/src/renderer