详解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的弹出框插件
Mar 18 Javascript
javascript读取xml实现javascript分页
Dec 13 Javascript
jQuery中toggleClass()方法用法实例
Jan 05 Javascript
JavaScript正则表达式中的ignoreCase属性使用详解
Jun 16 Javascript
JavaScript数组对象实现增加一个返回随机元素的方法
Jul 27 Javascript
基于Node.js实现nodemailer邮件发送
Jan 26 Javascript
JS脚本实现动态给标签控件添加事件的方法
Jun 02 Javascript
js字符串操作总结(必看篇)
Nov 22 Javascript
fullPage.js和CSS3实现全屏滚动效果
May 05 Javascript
cropper js基于vue的图片裁剪上传功能的实现代码
Mar 01 Javascript
js打开word文档预览操作示例【不是下载】
May 23 Javascript
Vuex的热更替如何实现
Jun 05 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 is_file 判断给定文件名是否为一个正常的文件
2010/05/10 PHP
PHP输入流php://input介绍
2012/09/18 PHP
PHP中比较两个字符串找出第一个不同字符位置例子
2014/04/08 PHP
PHP检测移动设备类mobile detection使用实例
2014/04/14 PHP
PHP实现文件下载断点续传详解
2014/10/15 PHP
php获取当月最后一天函数分享
2015/02/02 PHP
浅谈php数组array_change_key_case() 函数和array_chunk()函数
2016/10/22 PHP
Yii框架批量插入数据扩展类的简单实现方法
2017/05/23 PHP
Track Image Loading效果代码分析
2007/08/13 Javascript
javascript自执行函数之伪命名空间封装法
2010/12/25 Javascript
深入理解JavaScript系列(10) JavaScript核心(晋级高手必读篇)
2012/01/15 Javascript
JavaScript操作cookie类实例
2015/03/31 Javascript
jquery实现Ctrl+Enter提交表单的方法
2015/07/21 Javascript
浅谈javascript中的数据类型转换
2016/12/27 Javascript
vue引入swiper插件的使用实例
2017/07/19 Javascript
CSS3结合jQuery实现动画效果及回调函数的实例
2017/12/27 jQuery
vue2.0路由切换后页面滚动位置不变BUG的解决方法
2018/03/14 Javascript
详解vue.js根据不同环境(正式、测试)打包到不同目录
2018/07/13 Javascript
layui数据表格重载实现往后台传参
2019/11/15 Javascript
js实现双色球效果
2020/08/02 Javascript
JS跨浏览器解析XML应用过程详解
2020/10/16 Javascript
vue 判断两个时间插件结束时间必选大于开始时间的代码
2020/11/04 Javascript
基于ajax实现上传图片代码示例解析
2020/12/03 Javascript
Python中的两个内置模块介绍
2015/04/05 Python
python使用itchat库实现微信机器人(好友聊天、群聊天)
2018/01/04 Python
python中的selenium安装的步骤(浏览器自动化测试框架)
2020/03/17 Python
CSS3控制HTML元素动画效果
2014/02/08 HTML / CSS
Under Armour瑞典官方网站:美国高端运动科技品牌
2018/11/21 全球购物
ManoMano英国:欧洲第一家专注于DIY和园艺市场的电商平台
2020/03/12 全球购物
高校十八大报告感想
2014/01/27 职场文书
销售顾问工作计划书
2014/08/15 职场文书
尊师重教演讲稿
2014/09/04 职场文书
2014年村计划生育工作总结
2014/11/14 职场文书
入党现实表现材料
2014/12/23 职场文书
2015年度个人思想工作总结
2015/04/08 职场文书
离婚答辩状怎么写
2015/05/22 职场文书