Appearance
PatchFlags优化
Diff算法无法避免新旧虚拟DOM中无用的比较操作,通过patchFlags来标记动态内容,可以实现快速diff算法
<div>
<h1>Hello Jiang</h1>
<span>{{name}}</span>
</div>
1
2
3
4
2
3
4
此template经过模板编译会变成以下代码:
const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("h1", null, "Hello Jiang"),
_createTextVNode(),
_createElementVNode("span", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
]))
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
创建虚拟节点
const VueComponent = {
setup(){
let state = reactive({name:'jw'});
setTimeout(() => {
state.name = 'zf'
}, 1000);
return {
...toRefs(state)
}
},
render(_ctx){
return (openBlock(),createElementBlock('div',null,[
createElementVNode("h1", null, "Hello Jiang"),
createElementVNode("span", null, toDisplayString(_ctx.name), 1 /* TEXT */)
]))
}
}
render(h(VueComponent),app)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
生成的虚拟DOM是:
{
type: "div",
__v_isVNode: true,
children:[
{type: 'h1', props: null, key: null, …}
{type: Symbol(), props: null, key: null, …}
{type: 'span', props: null, key: null, …}
],
dynamicChildren:[{type: 'span', children: _ctx.name, patchFlag: 1}]
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
此时生成的虚拟节点多出一个dynamicChildren属性。这个就是block的作用,block可以收集所有后代动态节点。这样后续更新时可以直接跳过静态节点,实现靶向更新
动态标识
export const enum PatchFlags {
TEXT = 1, // 动态文本节点
CLASS = 1 << 1, // 动态class
STYLE = 1 << 2, // 动态style
PROPS = 1 << 3, // 除了class\style动态属性
FULL_PROPS = 1 << 4, // 有key,需要完整diff
HYDRATE_EVENTS = 1 << 5, // 挂载过事件的
STABLE_FRAGMENT = 1 << 6, // 稳定序列,子节点顺序不会发生变化
KEYED_FRAGMENT = 1 << 7, // 子节点有key的fragment
UNKEYED_FRAGMENT = 1 << 8, // 子节点没有key的fragment
NEED_PATCH = 1 << 9, // 进行非props比较, ref比较
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
DEV_ROOT_FRAGMENT = 1 << 11,
HOISTED = -1, // 表示静态节点,内容变化,不比较儿子
BAIL = -2 // 表示diff算法应该结束
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
靶向更新实现
export { createVnode as createElementVNode }
let currentBlock = null
export function openBlock(){ // 创建block
currentBlock = []
}
export function closeBlock(){ //关闭block
currentBlock = null;
}
export function createElementBlock(type,props?,children?,patchFlag?){ // 创建block元素
return setupBlock(createVNode(type,props,children,patchFlag))// 将动态元素挂载到block节点上
}
export function setupBlock(vnode){
vnode.dynamicChildren = currentBlock;
closeBlock();
return vnode;
}
export function createTextVNode(text: ' ', flag = 0) { // 创建文本虚拟节点
return createVNode(Text, null, text, flag)
}
export function toDisplayString(val){ // 就是JSON.stringify
return isString(val)? val : val == null ? '' :isObject(val)? JSON.stringify(val): String(val);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const createVNode = (type,props,children = null,patchFlag =0)=>{
// ...
if(currentBlock && vnode.patchFlag > 0){
currentBlock.push(vnode);
}
return vnode;
}
1
2
3
4
5
6
7
2
3
4
5
6
7
靶向更新
const patchElement = (n1,n2) =>{ // 比较两个元素的差异
let el = (n2.el = n1.el);
const oldProps = n1.props || {}
const newProps = n2.props || {}
let {patchFlag} = n2;
if(patchFlag){ // 单独处理标识属性
if(patchFlag & PatchFlags.CLASS){
if(oldProps.class !== newProps.class){
hostPatchProp(el,'class',null,newProps.class);
}
}
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children)
}
}
}else{ // 处理所有属性
patchProps(oldProps,newProps,el);
}
if(n2.dynamicChildren){ // 比较动态节点
patchBlockChildren(n1,n2);
}else{
patchChildren(n1,n2,el);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function patchBlockChildren(n1,n2){
for(let i = 0 ; i < n2.dynamicChildren.length;i++){
patchElement(n1.dynamicChildren[i],n2.dynamicChildren[i]);
}
}
1
2
3
4
5
2
3
4
5
由此可以看出性能被大幅度提升,从tree级别的比对,变成了线性结构比对。
BlockTree
为什么我们还要提出blockTree的概念? 只有block不就挺好的么? 问题出在block在收集动态节点时是忽略虚拟DOM树层级的。
<div>
<p v-if="flag">
<span>{{a}}</span>
</p>
<div v-else>
<span>{{a}}</span>
</div>
</div>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
这里我们知道默认根节点是一个block节点,如果要是按照之前的套路来搞,这时候切换flag的状态将无法从p标签切换到div标签。 解决方案:就是将不稳定的结构也作为block来进行处理
不稳定结构
所谓的不稳结构就是DOM树的结构可能会发生变化。不稳定结构有哪些呢? (v-if/v-for/Fragment)
v-if
<div>
<div v-if="flag">
<span>{{a}}</span>
</div>
<div v-else>
<p><span>{{a}}</span></p>
</div>
</div>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
编译后的结果:
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
(_ctx.flag)
? (_openBlock(), _createElementBlock("div", { key: 0 }, [
_createElementVNode("span", null, _toDisplayString(_ctx.a), 1 /* TEXT */)
]))
: (_openBlock(), _createElementBlock("div", { key: 1 }, [
_createElementVNode("p", null, [
_createElementVNode("span", null, _toDisplayString(_ctx.a), 1 /* TEXT */)
])
]))
]))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Block(div)
Blcok(div,{key:0})
Block(div,{key:1})
1
2
3
2
3
父节点除了会收集动态节点之外,也会收集子block。 更新时因key值不同会进行删除重新创建
v-for
随着v-for
变量的变化也会导致虚拟DOM树变得不稳定
<div>
<div v-for="item in fruits">{{item}}</div>
</div>
1
2
3
2
3
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.fruits, (item) => {
return (_openBlock(), _createElementBlock("div", null, _toDisplayString(item), 1 /* TEXT */))
}), 256 /* UNKEYED_FRAGMENT */))
}
1
2
3
4
5
2
3
4
5
可以试想一下,如果不增加这个block,前后元素不一致是无法做到靶向更新的。因为dynamicChildren中还有可能有其他层级的元素。同时这里还生成了一个Fragment,因为前后元素个数不一致,所以称之为不稳定序列。
稳定Fragment
这里是可以靶向更新的, 因为稳定则有参照物
<div>
<div v-for="item in 3">{{item}}</div>
</div>
1
2
3
2
3
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(), _createElementBlock(_Fragment, null, _renderList(3, (item) => {
return _createElementVNode("div", null, _toDisplayString(item), 1 /* TEXT */)
}), 64 /* STABLE_FRAGMENT */))
]))
}
1
2
3
4
5
6
7
2
3
4
5
6
7
静态提升
<div>
<span>hello</span>
<span a=1 b=2>{{name}}</span>
<a><span>{{age}}</span></a>
</div>
1
2
3
4
5
2
3
4
5
我们把模板直接转化成render函数是这个酱紫的,那么问题就是每次调用render
函数都要重新创建虚拟节点。
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "hello"),
_createElementVNode("span", {
a: "1",
b: "2"
}, _toDisplayString(_ctx.name), 1 /* TEXT */),
_createElementVNode("a", null, [
_createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
])
]))
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "hello", -1 /* HOISTED */)
const _hoisted_2 = {
a: "1",
b: "2"
}
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createElementVNode("span", _hoisted_2, _toDisplayString(_ctx.name), 1 /* TEXT */),
_createElementVNode("a", null, [
_createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
])
]))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
静态提升则是将静态的节点或者属性提升出去。静态提升是以树为单位。也就是说树中节点有动态的不会进行提升。
预字符串化
静态提升的节点都是静态的,我们可以将提升出来的节点字符串化。 当连续静态节点超过20个时,会将静态节点序列化为字符串。
<div>
<span></span>
...
...
<span></span>
</div>
1
2
3
4
5
6
2
3
4
5
6
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>....</span>", 20)
1
缓存函数
<div @click="e=>v=e.target.value"></div>
1
每次调用render的时都要创建新函数
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", {
onClick: e=>_ctx.v=e.target.value
}, null, 8 /* PROPS */, ["onClick"]))
}
1
2
3
4
5
2
3
4
5
开启函数缓存后,函数会被缓存起来,后续可以直接使用
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", {
onClick: _cache[0] || (_cache[0] = e=>_ctx.v=e.target.value)
}))
}
1
2
3
4
5
2
3
4
5