详解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 相关文章推荐
jQuery1.6 正式版发布并提供下载
May 05 Javascript
jQuery的each终止或跳过示例代码
Dec 12 Javascript
完美实现js焦点轮播效果(二)(图片可滚动)
Mar 07 Javascript
vue下跨域设置的相关介绍
Aug 26 Javascript
JS实现的按钮点击颜色切换功能示例
Oct 19 Javascript
web前端vue之vuex单独一文件使用方式实例详解
Jan 11 Javascript
vue.js响应式原理解析与实现
Jun 22 Javascript
node.js命令行教程图文详解
May 27 Javascript
layui table复选框禁止某几条勾选的实例
Sep 20 Javascript
小程序点餐界面添加购物车左右摆动动画
Sep 23 Javascript
js实现石头剪刀布游戏
Oct 11 Javascript
ajax jquery实现页面某一个div的刷新效果
Mar 04 jQuery
微信小程序实现滑动操作代码
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
收集的DedeCMS一些使用经验
2007/03/17 PHP
在JavaScript中调用php程序
2009/03/09 PHP
php中如何使对象可以像数组一样进行foreach循环
2013/08/09 PHP
PHP中trim()函数简单使用指南
2015/04/16 PHP
php 防止表单重复提交两种实现方法
2016/11/03 PHP
Javascript Global对象
2009/08/13 Javascript
JQuery小知识
2010/10/15 Javascript
js中的scroll和offset 使用比较的实例与分析
2013/09/29 Javascript
小结Node.js中非阻塞IO和事件循环
2014/09/18 Javascript
js文件包含的几种方式介绍
2014/09/28 Javascript
JavaScript通过function定义对象并给对象添加toString()方法实例分析
2015/03/23 Javascript
bootstrapValidator自定验证方法写法
2016/12/01 Javascript
vuex存储token示例
2019/11/11 Javascript
原生js实现五子棋游戏
2020/05/28 Javascript
vue离开当前页面触发的函数代码
2020/09/01 Javascript
Python使用minidom读写xml的方法
2015/06/03 Python
python学习笔记之调用eval函数出现invalid syntax错误问题
2015/10/18 Python
python脚本监控docker容器
2016/04/27 Python
Python自定义进程池实例分析【生产者、消费者模型问题】
2016/09/19 Python
Python将list中的string批量转化成int/float的方法
2018/06/26 Python
Python pymongo模块常用操作分析
2018/09/01 Python
python机器学习之神经网络实现
2018/10/13 Python
django框架基于模板 生成 excel(xls) 文件操作示例
2019/06/19 Python
详解Ubuntu环境下部署Django+uwsgi+nginx总结
2020/04/02 Python
python 基于selenium实现鼠标拖拽功能
2020/12/24 Python
瑜伽灵感珠宝:Satya Jewelry
2018/01/06 全球购物
惠而浦美国官网:Whirlpool.com
2021/01/19 全球购物
盛大笔试题
2016/11/05 面试题
机械系大学毕业生推荐信
2013/11/27 职场文书
网络专业学生个人的自我评价
2013/12/16 职场文书
2014房屋登记授权委托书
2014/10/13 职场文书
小学语文教学随笔
2015/08/14 职场文书
2016教师年度考核评语大全
2015/12/01 职场文书
2019升学宴主持词范本5篇
2019/10/09 职场文书
使用Oracle命令进行数据库备份与还原
2021/12/06 Oracle
SQL Server使用CROSS APPLY与OUTER APPLY实现连接查询
2022/05/25 SQL Server