详解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插件集合
Jan 12 Javascript
js中将字符串转换成json的三种方式
Jan 12 Javascript
JS判断页面加载状态以及添加遮罩和缓冲动画的代码
Oct 11 Javascript
3分钟写出来的Jquery版checkbox全选反选功能
Oct 23 Javascript
JS实现选项卡实例详解
Nov 17 Javascript
Javascript实现Array和String互转换的方法
Dec 21 Javascript
js提交form表单,并传递参数的实现方法
May 25 Javascript
JS原型对象的创建方法详解
Jun 16 Javascript
用JS实现图片轮播效果代码(一)
Jun 26 Javascript
vue实现图片加载完成前的loading组件方法
Feb 05 Javascript
node.js中TCP Socket多进程间的消息推送示例详解
Jul 10 Javascript
js module大战
Apr 19 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 上传文件大小限制
2009/07/05 PHP
php生成图形(Libchart)实例
2013/11/06 PHP
PHP实现 APP端微信支付功能
2018/06/22 PHP
jquery实现点击TreeView文本父节点展开/折叠子节点
2013/01/10 Javascript
html+javascript实现可拖动可提交的弹出层对话框效果
2013/08/05 Javascript
初识Node.js
2015/03/20 Javascript
不能不知道的10个angularjs英文学习网站
2016/03/23 Javascript
Javascript随机标签云代码实例
2016/06/21 Javascript
js友好的时间返回函数
2016/08/24 Javascript
Vue.js事件处理器与表单控件绑定详解
2017/03/20 Javascript
js实现放大镜特效
2017/05/18 Javascript
jQuery 实时保存页面动态添加的数据的示例
2017/08/14 jQuery
js原生方法被覆盖,从新赋值原生的方法
2018/01/02 Javascript
nodejs 使用 js 模块的方法实例详解
2018/12/04 NodeJs
JavaScript数据结构与算法之二叉树插入节点、生成二叉树示例
2019/02/21 Javascript
JavaScript实现汉字转换为拼音及缩写的方法示例
2019/03/28 Javascript
react 移动端实现列表左滑删除的示例代码
2019/07/04 Javascript
python的dict,set,list,tuple应用详解
2014/07/24 Python
Python 正则表达式入门(初级篇)
2016/12/07 Python
利用Python查看微信共同好友功能的实现代码
2019/04/24 Python
int在python中的含义以及用法
2019/06/27 Python
python如何爬取网站数据并进行数据可视化
2019/07/08 Python
基于Python新建用户并产生随机密码过程解析
2019/10/08 Python
解决pandas展示数据输出时列名不能对齐的问题
2019/11/18 Python
TensorFlow命名空间和TensorBoard图节点实例
2020/01/23 Python
HTML5中外部浏览器唤起微信分享功能的代码
2020/09/15 HTML / CSS
美国知名的女性服饰品牌:LOFT(洛芙特)
2016/08/05 全球购物
为什么Runtime.exec(“ls”)没有任何输出?
2014/10/03 面试题
学生个人的自我评价分享
2013/11/05 职场文书
党员公开承诺书范文
2014/03/25 职场文书
团队激励口号
2014/06/06 职场文书
学习礼仪心得体会
2014/09/01 职场文书
公司授权委托书范文
2014/09/21 职场文书
团员自我评价范文
2015/03/10 职场文书
毕业班班主任工作总结2015
2015/07/23 职场文书
pytest进阶教程之fixture函数详解
2021/03/29 Python