详解Vue3中对VDOM的改进


Posted in Javascript onApril 23, 2020

前言

vue-next 对virtual dom的patch更新做了一系列的优化,从编译时加入了 block 以减少 vdom 之间的对比次数,另外还有 hoisted 的操作减少了内存的开销。本文写给自己看,做个知识点记录,如有错误,还请不吝赐教。

VDOM

VDOM的概念简单来说就是用js对象来模拟真实DOM树。由于MV**的架构,真实DOM树应该随着数据(Vue2.x中的data)的改变而发生改变,这些改变可能是以下几个方面:

  • v-if
  • v-for
  • 动态的props(如:class,@click)
  • 子节点的改变
  • 等等

Vue框架要做的其实很单一:在用户改变数据时,正确更新DOM树,做法就是其核心的VDOM的patch和diff算法。

Vue2.x中的做法

在Vue2.x中,当数据改变后就要对所有的节点进行patch和diff操作。如以下DOM结构:

<div>
 <span class="header">I'm header</span>
 <ul>
  <li>第一个静态li</li>
  <li v-for="item in mutableItems" :key="item.key"> {{ item.desc }}</li>
 </ul>
</div>

在第一次mount节点的时候会去生成真实的DOM,此后如果

mutableItems.push({
 key: 'asdf',
 desc: 'a new li item'
})

预期的结果是页面出现新的一个li元素,内容就是 a new li item,Vue2.x中是通过patch时对 ul 元素对应的 vnode 的 children 来进行 diff 操作,具体操作在此不深究,但是该操作是需要比较所有的 li 对应的 vnode 的。

不足

正是由于2.x版本中的diff操作需要遍历所有元素,本例中包括了 span 和 第一个li元素,但是这两个元素是静态的,不需要被比较的,不论数据怎么变,静态元素都不会再更改了。vue-next在编译时对这种操作做了优化,即 Block。

Block

入上述模板,在vue-next中生成的渲染函数为:

const _Vue = Vue
const { createVNode: _createVNode } = _Vue

const _hoisted_1 = _createVNode("span", { class: "header" }, "I'm header", -1 /* HOISTED */)
const _hoisted_2 = _createVNode("li", null, "第一个静态li", -1 /* HOISTED */)

return function render(_ctx, _cache) {
 with (_ctx) {
  const { createVNode: _createVNode, renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, toDisplayString: _toDisplayString } = _Vue

  return (_openBlock(), _createBlock(_Fragment, null, [
   _hoisted_1,
   _createVNode("ul", null, [
    _hoisted_2,
    (_openBlock(true), _createBlock(_Fragment, null, _renderList(state.mutableItems, (item) => {
     return (_openBlock(), _createBlock("li", { key: item.key }, _toDisplayString(item.desc), 1 /* TEXT */))
    }), 128 /* KEYED_FRAGMENT */))
   ])
  ], 64 /* STABLE_FRAGMENT */))
 }
}

我们可以看到调用了 openBlock 和 createBlock 方法,这两个方法的代码实现也很简单:

const blockStack: (VNode[] | null)[] = []
let currentBlock: VNode[] | null = null
let shouldTrack = 1
// openBlock
export function openBlock(disableTracking = false) {
 blockStack.push((currentBlock = disableTracking ? null : []))
}
export function createBlock(
 type: VNodeTypes | ClassComponent,
 props?: { [key: string]: any } | null,
 children?: any,
 patchFlag?: number,
 dynamicProps?: string[]
): VNode {
 // avoid a block with patchFlag tracking itself
 shouldTrack--
 const vnode = createVNode(type, props, children, patchFlag, dynamicProps)
 shouldTrack++
 // save current block children on the block vnode
 vnode.dynamicChildren = currentBlock || EMPTY_ARR
 // close block
 blockStack.pop()
 currentBlock = blockStack[blockStack.length - 1] || null
 // a block is always going to be patched, so track it as a child of its
 // parent block
 if (currentBlock) {
  currentBlock.push(vnode)
 }
 return vnode
}

更加详细的注释还请看源代码中的注释,写的十分详尽,便于理解。这里面 openBlock 就是初始化一个块,createBlock 就是对当前编译的内容生成一个块,这里面的这一行代码:vnode.dynamicChildren = currentBlock || EMPTY_ARR 就是在收集动态的子节点,我们可以再看一下编译时运行的函数:

// createVNode
function _createVNode(
 type: VNodeTypes | ClassComponent,
 props: (Data & VNodeProps) | null = null,
 children: unknown = null,
 patchFlag: number = 0,
 dynamicProps: string[] | null = null
) {
 /**
  * 一系列代码
 **/

 // presence of a patch flag indicates this node needs patching on updates.
 // component nodes also should always be patched, because even if the
 // component doesn't need to update, it needs to persist the instance on to
 // the next vnode so that it can be properly unmounted later.
 if (
  shouldTrack > 0 &&
  currentBlock &&
  // the EVENTS flag is only for hydration and if it is the only flag, the
  // vnode should not be considered dynamic due to handler caching.
  patchFlag !== PatchFlags.HYDRATE_EVENTS &&
  (patchFlag > 0 ||
   shapeFlag & ShapeFlags.SUSPENSE ||
   shapeFlag & ShapeFlags.STATEFUL_COMPONENT ||
   shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
 ) {
  currentBlock.push(vnode)
 }
}

上述函数是在模板编译成ast之后调用的生成VNode的函数,所以有patchFlag这个标志,如果是动态的节点,并且此时是开启了Block的话,就会将节点塞入Block中,这样 createBlock返回的 VNode 中就会有 dynamicChildren 了。

到此为止,通过本文中案例经过模板编译和render函数运行后并经过了优化以后生成了如下结构的vnode:

const result = {
 type: Symbol(Fragment),
 patchFlag: 64,
 children: [
  { type: 'span', patchFlag: -1, ...},
  {
   type: 'ul',
   patchFlag: 0,
   children: [
    { type: 'li', patchFlag: -1, ...},
    {
     type: Symbol(Fragment),
     children: [
      { type: 'li', patchFlag: 1 ...},
      { type: 'li', patchFlag: 1 ...}
     ]
    }
   ]
  }
 ],
 dynamicChildren: [
  {
   type: Symbol(Fragment),
   patchFlag: 128,
   children: [
    { type: 'li', patchFlag: 1 ...},
    { type: 'li', patchFlag: 1 ...}
   ]
  }
 ]
}

以上的 result 不完整,但是我们暂时只关心这些属性。可以看见 result.children 的第一个元素是span,patchFlag=-1,且 result 有一个 dynamicChildren 数组,里面只包含了两个动态的 li,后续如果变动了数据,那么新的 vnode.dynamicChildren 会有第三个 li 元素。

patch

patch部分其实也没差多少,就是根据vnode的type执行不同的patch操作:

function patchElement(n1, n2) {
 let { dynamicChildren } = n2
 // 一系列操作

 if (dynamicChildren) {
  patchBlockChildren (
   n1.dynamicChildren!,
   dynamicChildren,
   el,
   parentComponent,
   parentSuspense,
   areChildrenSVG
  )
 } else if (!optimized) {
  // full diff
  patchChildren(
   n1,
   n2,
   el,
   null,
   parentComponent,
   parentSuspense,
   areChildrenSVG
  )
 }
}

可以看见,如果有了 dynamicChildren 那么vue2.x版本中的diff操作就被替换成了 patchBlockChildren() 且参数只有 dynamicChildren,就是静态的不做diff操作了,而如果vue-next的patch中没有 dynamicChildren,则进行完整的diff操作,入注释写的 full diff 的后续代码。

结尾

本文没有深入讲解代码的实现层面,一是因为自己实力不济还在阅读源码当中,二是我个人认为阅读源码不可钻牛角尖,从大局入眼,再徐徐图之,先明白了各个部分的作用后带着思考去阅读源码能收获到的应该更多一些。

到此这篇关于详解Vue3中对VDOM的改进的文章就介绍到这了,更多相关Vue3 VDOM内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
Jquery 获得服务器控件值的方法小结
May 11 Javascript
jquery选择器(常用选择器说明)
Sep 28 Javascript
jQuery的控件及事件(输入控件及回车事件)使用示例
Jul 25 Javascript
JQuery 使用attr方法实现下拉列表选中
Oct 13 Javascript
javascript实现节点(div)名称编辑
Dec 17 Javascript
微信小程序 支付简单实例及注意事项
Jan 06 Javascript
vue小图标favicon不显示的解决方案
Sep 19 Javascript
three.js加载obj模型的实例代码
Nov 10 Javascript
Vue.js+Layer表格数据绑定与实现更新的实例
Mar 07 Javascript
详解Vue实战指南之依赖注入(provide/inject)
Nov 13 Javascript
微信小程序图片加载失败时替换为默认图片的方法
Dec 09 Javascript
Angular5整合富文本编辑器TinyMCE的方法(汉化+上传)
May 26 Javascript
微信小程序实现滑动操作代码
Apr 23 #Javascript
微信小程序图片右边加两行文字的代码
Apr 23 #Javascript
Vue中通过vue-router实现命名视图的问题
Apr 23 #Javascript
利用原生JS实现欢乐水果机小游戏
Apr 23 #Javascript
JS eval代码快速解密实例解析
Apr 23 #Javascript
浅谈vue权限管理实现及流程
Apr 23 #Javascript
js实现简单的贪吃蛇游戏
Apr 23 #Javascript
You might like
php 短链接算法收集与分析
2011/12/30 PHP
Laravel框架中实现使用阿里云ACE缓存服务
2015/02/10 PHP
PHPExcel实现的读取多工作表操作示例
2020/04/14 PHP
imagettftext() 失效,不起作用
2021/03/09 PHP
用javascript实现给出的盒子的序列是否可连为一矩型
2007/08/30 Javascript
javascript 弹出窗口中是否显示地址栏的实现代码
2011/04/14 Javascript
关于extjs treepanel复选框选中父节点与子节点的问题
2013/04/02 Javascript
基于JQuery实现的Select级联
2014/01/27 Javascript
js限制checkbox选中个数以限制六个为例
2014/07/15 Javascript
JavaScript中常见获取元素的方法汇总
2015/03/04 Javascript
跟我学习javascript的prototype原型和原型链
2015/11/18 Javascript
Node.js操作mysql数据库增删改查
2016/03/30 Javascript
jQuery EasyUI学习教程之datagrid点击列表头排序
2016/07/09 Javascript
原生JS实现日历组件的示例代码
2017/09/22 Javascript
JS实现简单获取最近7天和最近3天日期的方法
2018/04/18 Javascript
Vue实现todolist删除功能
2018/06/26 Javascript
layer关闭当前窗口页面以及确认取消按钮的方法
2019/09/09 Javascript
Element-ui树形控件el-tree自定义增删改和局部刷新及懒加载操作
2020/08/31 Javascript
Python的爬虫包Beautiful Soup中用正则表达式来搜索
2016/01/20 Python
举例讲解Python中的迭代器、生成器与列表解析用法
2016/03/20 Python
Python复制Word内容并使用格式设字体与大小实例代码
2018/01/22 Python
python实现机器学习之元线性回归
2018/09/06 Python
10招!看骨灰级Pythoner玩转Python的方法
2019/04/15 Python
Python面向对象之类和实例用法分析
2019/06/08 Python
django 通过url实现简单的权限控制的例子
2019/08/16 Python
Python连接Hadoop数据中遇到的各种坑(汇总)
2020/04/14 Python
python opencv 实现读取、显示、写入图像的方法
2020/06/08 Python
Python之Matplotlib文字与注释的使用方法
2020/06/18 Python
基于Python pyecharts实现多种图例代码解析
2020/08/10 Python
Python如何急速下载第三方库详解
2020/11/02 Python
美国气象仪器、花园装饰和墙壁艺术商店:Wind & Weather
2019/05/29 全球购物
采购员岗位职责
2013/11/15 职场文书
2014年小学校长工作总结
2014/12/08 职场文书
人与自然的观后感
2015/06/18 职场文书
小学生暑假生活总结
2015/07/13 职场文书
Java中的Kotlin 内部类原理
2022/06/16 Java/Android