关注前端公众号 【前端每日一博】

前言

面试官:

你知道 Vue3.0 Object.difineProperty和vue3.0的proxy的区别吗?

你知道 Vue3.0 diff算法原理和它有什么优化吗?

你知道 Vue3.0 proxy是如何通知相关组件进行重新渲染吗?

...

你:。。。

随着vue3.0beta版本的发布,vue3.0正式版本相信不久就会与我们相遇。尤雨溪在直播中也说了vue3.0的新特性typescript强烈支持,proxy响应式原理,重新虚拟dom,优化diff算法性能提升等等。可想而知,Vue3.0时代有一大波我们需要了解的东西。

「废话不多说,今天的主题是和大家分享一下Vue 虚拟Dom、Diff算法原理以及源码解析、以及Vue3.0的diff算法是如何进行优化的。」

其他系列在此:

「Vue3.0时代你必须了解的:响应式原理」

「Vue3.0时代你必须了解的:compositon api 用法和注意事项」

「基于项目实战阐述vue3.0新型状态管理和逻辑复用方式」

目录

  1. virtual dom
  2. diff算法原理
  3. Vue3.0 diff算法优化

virtual dom

如果咱们不了解virtual dom 的话,要理解DIff算法是比较困难的。

概念:

  1. template
  2. 渲染函数
  3. vnode(virtual dom)
  4. patch(diff算法)
  5. view

  • Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树

  • VNode 虚拟节点:它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。

  • patch(也叫做patching算法):虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。Vue的Virtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进。

什么是virtual dom

通俗易懂的来说就是用一个简单的对象去代替复杂的dom对象。

如果你去打印一下一个真实的DOM节点,就会发现DOM节点上有很多属性,如果Vue每次都生成一个新的真实DOM节点,对性能是巨大的浪费。

Virtual DOM 实际上就是以JavaScript对象(VNode )为基础形成一棵树,对真实DOM的一层抽象。Vue最终的工作就是通过这棵树批量生成真实的DOM节,可以说两者存在一层映射关系。

简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。

对于虚拟DOM,咱们来看一个简单的实例,就是下图所示的这个,详细的阐述了模板 → 渲染函数 → 虚拟DOM树 → 真实DOM的一个过程


其实虚拟DOM在Vue.js主要做了两件事:

  • 提供与真实DOM节点所对应的虚拟节点vnode
  • 将虚拟节点vnode和旧虚拟节点oldVnode进行对比(diff算法),然后更新视图

总结:「vdom是为了减轻性能压力。dom是昂贵的,昂贵的一方面在dom本身的重量,dom节点在js里的描述是个非常复杂属性很多原型很长的超级对象,另一方面是浏览器渲染进程和js进程是独立分离的,操作dom时的通信和浏览器本身需要重绘的时耗都是很高的。所以大家机智的搞了个轻量的vdom去模拟dom,vdom每个节点都只挂载js操作的必要属性,每次组件update时都先操作vdom,通过vdom的比对,得到一个真实dom的需要操作的集合。整个机制是在JavaScript层面计算,在完成之前并不会操作DOM,等数据稳定之后再实现精准的修改。」

分析diff算法

由上我们知道了,新的虚拟DOM和旧的虚拟DOm是通过diff算法进行比较之后更新的。

Vue2.x diff算法

Vue2.x diff算法原理

传统diff算法通过循环递归对节点进行依次对比效率低下,算法复杂度达到O(N^3),主要原因在于其追求完全比对和最小修改,而React、Vue则是放弃了完全比对及最小修改,才实现从O(N^3) => O(N)。

优化措施有:

  • 「分层diff」:不考虑跨层级移动节点,让新旧两个VDOM树的比对无需循环递归(复杂度大幅优化,直接下降一个数量级的首要条件)。这个前提也是Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。

在同层节点中,采用了「双端比较的算法」过程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较;


当发生以下情况则跳过比对,变为插入或删除操作:

  • 「组件的Type(Tagname)不一致」,原因是绝大多数情况拥有相同type的两个组件将会生成相似的树形结构,拥有不同type的两个组件将会生成不同的树形结构,所以type不一致可以放弃继续比对。

  • 「列表组件的Key不一致」,旧树中无新Key或反之。毕竟key是元素的身份id,能直接对应上是否是同一个节点。

  • 对触发了getter/setter 的组件进行diff,精准减少diff范围

Vue3.0 diff

diff痛点

vue2.x中的虚拟dom是进行「全量的对比」,在运行时会对所有节点生成一个虚拟节点树,当页面数据发生变更好,会遍历判断virtual dom所有节点(包括一些不会变化的节点)有没有发生变化;虽然说diff算法确实减少了多DOM节点的直接操作,但是这个「减少是有成本的」,如果是复杂的大型项目,必然存在很复杂的父子关系的VNode,「而Vue2.x的diff算法,会不断地递归调用 patchVNode,不断堆叠而成的几毫秒,最终就会造成 VNode 更新缓慢」

那么Vue3.0是如何解决这些问题的呢

动静结合 PatchFlag

来个?:

{msg}
静态文字


在Vue3.0中,在这个模版编译时,编译器会在动态标签末尾加上 /* Text*/ PatchFlag。「也就是在生成VNode的时候,同时打上标记,在这个基础上再进行核心的diff算法」并且 PatchFlag 会标识动态的属性类型有哪些,比如这里 的TEXT 表示只有节点中的文字是动态的。而patchFlag的类型也很多。这里直接引用一张图片。


其中大致可以分为两类:

  • 当 patchFlag 的值「大于」 0 时,代表所对应的元素在 patchVNode 时或 render 时是可以被优化生成或更新的。

  • 当 patchFlag 的值「小于」 0 时,代表所对应的元素在 patchVNode 时,是需要被 full diff,即进行递归遍历 VNode tree 的比较更新过程。

看源码:

export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [  _createVNode("p", null, "'HelloWorld'"),  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)

 ]))}****

这里的_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)就是对变量节点进行标记。

总结:「Vue3.0对于不参与更新的元素,做静态标记并提示,只会被创建一次,在渲染时直接复用。」

其中还有cacheHandlers(事件侦听器缓存),这里就不讲了。

diff算法源码解析

以数组为栗子:

newNode:[a,b,c,d,e,f,g] 

oldNode:[a,b,c,h,i,j,f,g]

步骤1:「从首部比较new vnode 和old vnode」,如果碰到不同的节点,跳出循环,否则继续,直到一方遍历完成;

由此我们得到newNode和oldNode首部相同的片段为 a,b,c,

源码:

const patchKeyedChildren = (    c1,    c2,    container,    parentAnchor,    parentComponent,    parentSuspense,    isSVG,    optimized) => {    let i = 0;      const l2 = c2.length    let e1 = c1.length - 1    let e2 = c2.length - 1

    while (i <= e1 && i <= e2) {      const n1 = c1[i]      const n2 = c2[i]      if (isSameVNodeType(n1, n2)) {        patch(          n1,          n2,          container,          parentAnchor,          parentComponent,          parentSuspense,          isSVG,          optimized        )      } else {        break      }      i++    }

    //这里的isSameVNodeType      export function isSameVNodeType(n1: VNode, n2: VNode): boolean {      // 比较类型和key是否一致()      return n1.type === n2.type && n1.key === n2.key      }

「Tip」:这里的isSameVNodeType从「type和key」,因此key作为唯一值是非常重要的,这也就解释了 v-for循环遍历不能用index作为key的原因。

步骤2:「从尾部比较new vnode 和old vnode」,如果碰到不同的节点,跳出循环,否则继续,直到一方遍历完成;

由此我们得到newNode和oldNode尾部相同的片段为 f,g

while (i <= e1 && i <= e2) {      const n1 = c1[e1]      const n2 = c2[e2]      if (isSameVNodeType(n1, n2)) {        patch(          n1,          n2,          container,          parentAnchor,          parentComponent,          parentSuspense,          isSVG,          optimized        )      } else {        break      }      e1--      e2--    }}

在遍历过程中满足i > e1 && i < e2,说明 「仅有节点新增」

if (i > e1) {    if (i <= e2) {        const nextPos = e2 + 1;        const anchor = nextPos         while (i <= e2) {            patch(                null,                c2[i],                container,                anchor,                parentComponent,                parentSuspense,                isSVG            )            i++        }    }} else if {    ...} else {   ...}

在遍历过程中满足i > e1 && i > e2,说明 「仅有节点移除」

if (i > e1) {  //} else if (i > e2) {    while (i <= e1) {        unmount(c1[i], parentComponent, parentSuspense, true)        i++    }} else {    //}

步骤3: 「节点移动、新增或删除」

经过以上步骤,剩下的就是不确定的元素,那么diff算法将遍历 所有的new node,将key和索引存在keyToNewIndexMap中,为map解构,

if (i > e1) {  //} else if (i > e2) {  //} else {    const s1 = i    const s2 = i

    const keyToNewIndexMap = new Map()    for (i = s2; i <= e2; i++) {        const nextChild = c2[i]        if (nextChild.key !== null) {            keyToNewIndexMap.set(nextChild.key, i)        }    }}

接下来

 for (i = s1; i <= e1; i++) { /* 开始遍历老节点 */        const prevChild = c1[i]        if (patched >= toBePatched) { /* 已经patch数量大于等于, */          /* ① 如果 toBePatched新的节点数量为0 ,那么统一卸载老的节点 */          unmount(prevChild, parentComponent, parentSuspense, true)          continue        }        let newIndex         /* ② 如果,老节点的key存在 ,通过key找到对应的index */        if (prevChild.key != null) {          newIndex = keyToNewIndexMap.get(prevChild.key)        } else { /*  ③ 如果,老节点的key不存在 */          for (j = s2; j <= e2; j++) { /* 遍历剩下的所有新节点 */            if (              newIndexToOldIndexMap[j - s2] === 0 && /* newIndexToOldIndexMap[j - s2] === 0 新节点没有被patch */              isSameVNodeType(prevChild, c2[j] as VNode)            ) { /* 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex  */              newIndex = j              break            }          }        }        if (newIndex === undefined) { /* ①没有找到与老节点对应的新节点,删除当前节点,卸载所有的节点 */          unmount(prevChild, parentComponent, parentSuspense, true)        } else {          /* ②把老节点的索引,记录在存放新节点的数组中, */          newIndexToOldIndexMap[newIndex - s2] = i + 1          if (newIndex >= maxNewIndexSoFar) {            maxNewIndexSoFar = newIndex          } else {            /* 证明有节点已经移动了   */            moved = true          }          /* 找到新的节点进行patch节点 */          patch(            prevChild,            c2[newIndex] as VNode,            container,            null,            parentComponent,            parentSuspense,            isSVG,            optimized          )          patched++        } }

之后通过 计算出「最长递增子序列」 减少Dom元素的移动,达到最少的 dom 操作。至于什么是最长递增子序列,有兴趣的小伙伴可以自信查阅。

最后

Vue3.0 的diff原理和优化也讲完了,如有错误,欢迎指出。

做diff_Vue3.0时代你必须了解的:diff算法原理和优化相关推荐

  1. 以太坊开启区块链2.0时代

    链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. 今天我们唠唠以太坊,这个开启区块链2.0时代的伟大项目. 前面我们说比特币实际上是一个大账本,从比特币白皮书里走出来的区 ...

  2. 圆桌讨论:Cloud 2.0时代的工业互联网与智能制造

    圆桌讨论实录,Rancher CEO梁胜博士主持,邀华为.上汽.金风.云宏共同畅聊对Kubernetes.Service Mesh等新技术的走向预判,容器与虚拟机之间的竞合,还有制造业龙头企业遇见的I ...

  3. 圆桌讨论:Cloud 2.0时代的工业互联网与智能制造 1

    2018年11月13日,企业级Kubernetes管理平台Rancher Labs携手华为,联合世界顶级容器盛会KubeCon+CloudNativeCon,共同举办了以Service Mesh为主题 ...

  4. 深度报告:芯片设计EDA 2.0时代,三大路径搞定六大挑战

    编辑:智东西内参 EDA是Electronic Design Automation的缩写,几十年来成为芯片设计模块.工具.流程的代称.从仿真.综合到版图,从前端到后端,从模拟到数字再到混合设计,以及工 ...

  5. FaaS如何在云2.0时代发挥优势,又将走向何方?

    摘要: 过去十年,云服务深刻地改变了社会获取和使用计算能力的方式,云服务自身也以极快的速度演进.在基础设施云化之后,容器.Serverless等技术迅猛发展,开始推动业务能力的云化,云计算进入2.0时 ...

  6. 地平线黄畅:软件2.0时代,数据驱动进化,算力将成为智能化的基石丨MEET2021...

    编辑部 整理自 MEET 2021 量子位 报道 | 公众号 QbitAI 当下,AI芯片将迎来什么样的挑战? 随着软件步入"2.0时代",数据开始驱动AI进化,算力也逐渐成为智能 ...

  7. 没有 4.21 ,Linus Torvalds 宣布 Linux 进入 5.0 时代

    开发四年只会写业务代码,分布式高并发都不会还做程序员?   按最初计划,Linux Kernel 5.0 本应在2018年年底发布,但由于种种原因,最终在去年圣诞节发布的是 Linux Kernel ...

  8. 探寻阿里云服务器迈入2.0时代的技术要点

    在10月15日举行的云栖大会上,阿里云宣布云服务器正式进入2.0时代.根据其官方发布的信息来看,本次ECS服务器在以下方面做了升级 将Xen切换到KVM,同时支持Xen和KVM两大虚拟化系统: 推出了 ...

  9. 话里话外:流程管理进入2.0时代

    流程管理在中国推广了十年.从1999年我们开始引进和翻译国外90年代兴起的BPR(流程再造)以来,中国企业对流程管理已不再陌生.而十年前我总要先向企业解释市场化的发展趋势和流程管理的价值.      ...

最新文章

  1. Nacos OPEN API配置管理测试
  2. GPU Gems1 - 22 颜色控制(Color Controls)
  3. java短除法获取二进制_Java十四天零基础入门-Java的数据类型介绍
  4. mcq 队列_人工智能搜索问题能力问题解答(MCQ)
  5. mysql php ajax_PHP 和 AJAX MySQL 数据库实例
  6. python生成热度图_Python数据可视化 热力图
  7. 阿里云携手印度电信巨头 网络互连覆盖150个国家地区
  8. Oracle12c操作命令
  9. 190602每日一句
  10. QQ空间内容同步php网站,同步 Sablog 博客日志到 Qzone
  11. 汽车Bootloader流程
  12. Shiro中自定义Realm
  13. 【转】2009最受欢迎中国技术博客评选(PB50)结果公布
  14. STM32使用正点原子无线烧录器无线查看数据波形
  15. 计算机主机上有几个按钮,电脑键盘各个按键有哪些作用 电脑键盘各个按键作用介绍...
  16. DHCP报文交换抓包分析
  17. 浅析swift optional
  18. 计算机专业可以当剪辑师,什么人可以做剪辑师?
  19. 深度技术 GHOST XP SP3 快速装机专业版 V2013.03
  20. python自动化(三)web自动化:2.web自动化工具selenium讲解

热门文章

  1. docker 报错 /usr/bin/docker-current: Error response from daemon: driver failed programming external
  2. linux 日志文件utmp、wtmp、lastlog、messages介绍
  3. linux c glob使用(文件路径模式查找函数)
  4. 内核模式下的注册表操作
  5. Android开发精要3--Android中的Intent机制
  6. Paoding Rose学习(一)
  7. Linux System Programming -- Appendix
  8. python数据存储与读取_【Python爬虫】数据保存与读取
  9. android linux应用安装位置,Android中App安装位置详解
  10. config文件_您自己的MicroProfile Config来源