探秘vue核心之虚拟DOM与diff

一、真实DOM和其解析流程

所有的浏览器渲染引擎工作流程大致分为5步:

创建 DOM 树 —> 创建 Style Rules -> 构建 Render 树 —> 布局 Layout -—> 绘制 Painting

  • 第一步,构建 DOM 树:用 HTML 分析器,分析 HTML 元素,构建一棵 DOM 树;
  • 第二步,生成样式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,生成页面的样式表;
  • 第三步,构建 Render 树:将 DOM 树和样式表关联起来,构建一棵 Render 树(Attachment)。每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象(又名 renderer),这些 render 对象最终会被构建成一棵 Render 树;
  • 第四步,确定节点坐标:根据 Render 树结构,为每个 Render 树上的节点确定一个在显示屏上出现的精确坐标;
  • 第五步,绘制页面:根据 Render 树和节点显示坐标,然后调用每个节点的 paint 方法,将它们绘制出来。

注意点:

1、DOM 树的构建是文档加载完成开始的? 构建 DOM 树是一个渐进过程,为达到更好的用户体验,渲染引擎会尽快将内容显示在屏幕上,它不必等到整个 HTML 文档解析完成之后才开始构建 render 树和布局。

2、Render 树是 DOM 树和 CSS 样式表构建完毕后才开始构建的? 这三个过程在实际进行的时候并不是完全独立的,而是会有交叉,会一边加载,一边解析,以及一边渲染。

3、CSS 的解析注意点? CSS 的解析是从右往左逆向解析的,嵌套标签越多,解析越慢。

4、JS 操作真实 DOM 的代价? 用我们传统的开发模式,原生 JSJQ 操作 DOM 时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。在一次操作中,我需要更新 10 个 DOM 节点,浏览器收到第一个 DOM 请求后并不知道还有 9 次更新操作,因此会马上执行流程,最终执行10 次。例如,第一次计算完,紧接着下一个 DOM 更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算 DOM 节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作 DOM 的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验

虚拟 DOM 的好处

​ 虚拟 DOM 就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有 10 次更新 DOM 的动作,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到本地一个 JS 对象中,最终将这个 JS 对象一次性 attchDOM 树上,再进行后续操作,避免大量无谓的计算量。所以,用 JS 对象模拟 DOM 节点的好处是,页面的更新可以先全部反映在 JS 对象(虚拟 DOM )上,操作内存中的 JS 对象的速度显然要更快,等更新完成后,再将最终的 JS 对象映射成真实的 DOM,交由浏览器去绘制。

二、什么是虚拟DOM

虚拟DOM是一个js 对象 一个用来表示真实DOM的对象 ,举个例子,请看以下真实DOM

<ul id="list"><li class="item">嘻嘻</li><li class="item">哈哈</li><li class="item">嘿嘿</li>
</ul>

对应的虚拟DOM为:

let oldVDOM = { // 旧虚拟DOMtagName: 'ul', // 标签名props: { // 标签属性id: 'list'},children: [ // 标签子节点{tagName: 'li', props: { class: 'item' }, children: ['嘻嘻']},{tagName: 'li', props: { class: 'item' }, children: ['哈哈']},{tagName: 'li', props: { class: 'item' }, children: ['嘿嘿']},]}

这时候,我修改一个li标签的文本

<ul id="list"><li class="item">嘻嘻</li><li class="item">哈哈</li><li class="item">啊哈哈哈哈哈哈哈哈</li> // 修改
</ul>

这时候生成的新虚拟DOM为:

let newVDOM = { // 新虚拟DOMtagName: 'ul', // 标签名props: { // 标签属性id: 'list'},children: [ // 标签子节点{tagName: 'li', props: { class: 'item' }, children: ['嘻嘻']},{tagName: 'li', props: { class: 'item' }, children: ['哈哈']},{tagName: 'li', props: { class: 'item' }, children: ['啊哈哈哈哈哈哈哈哈']},]}

这就是咱们平常说的新旧两个虚拟DOM,这个时候的新虚拟DOM是数据的最新状态,那么我们直接拿新虚拟DOM去渲染成真实DOM的话,效率真的会比直接操作真实DOM高吗?那肯定是不会的,看下图:

由上图,一看便知,肯定是第2种方式比较快,因为第1种方式中间还夹着一个虚拟DOM的步骤,所以虚拟DOM比真实DOM快这句话其实是错的,或者说是不严谨的。

那正确的说法是什么呢?虚拟DOM算法操作真实DOM,性能高于直接操作真实DOM

虚拟DOM虚拟DOM算法是两种概念。虚拟DOM算法 = 虚拟DOM + Diff算法

Vue.js 利用 createElement 方法创建 VNode。就是描述真实节点的js对象

tag: 当前节点的标签名
data: 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息
children: 当前节点的子节点,是一个数组
text: 当前节点的文本
elm: 当前虚拟节点对应的真实dom节点
ns: 当前节点的名字空间
context: 当前节点的编译作用域
functionalContext: 函数化组件作用域
key: 节点的key属性,被当作节点的标志,用以优化
componentOptions: 组件的option选项
componentInstance: 当前节点对应的组件的实例
parent: 当前节点的父节点
raw: 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false
isStatic: 是否为静态节点
isRootInsert: 是否作为跟节点插入
isComment: 是否为注释节点
isCloned: 是否为克隆节点
isOnce: 是否有v-once指令

三、什么是Diff算法

Diff 算法,在 Vue 里面就是叫做 patch ,它的核心就是参考 Snabbdom,通过新旧虚拟 DOM 对比(即 patch 过程),找出最小变化的地方转为进行 DOM 操作

  • snabbdom是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了snabbdom
  • 官方git: https://github.com/snabbdom/snabbdom

diff 算法用来比较两棵 Virtual DOM 树的差异,如果需要两棵树的完全比较,**那么 diff 算法的时间复杂度为O(n^3)。**但是在前端当中,你很少会跨越层级地移动 DOM 元素,所以 Virtual DOM 只会对同一个层级的元素进行对比,如下图所示, div 只会和同一层级的 div 对比,第二层级的只会跟第二层级对比,这样算法复杂度就可以达到 O(n)

扩展
在 Vue1 里是没有 patch 的,每个依赖都有单独的 Watcher 负责更新,当项目规模变大的时候性能就跟不上了,所以在 Vue2 里为了提升性能,改为每个组件只有一个 Watcher,那我们需要更新的时候,怎么才能精确找到组件里发生变化的位置呢?所以 patch 来了


那么它是在什么时候执行的呢?

在页面首次渲染的时候会调用一次 patch 并创建新的 vnode,不会进行更深层次的比较

总结:Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,而不用更新其他数据没发生改变的节点,实现精准地更新真实DOM,进而提高效率

四、Diff算法原理

Diff同层对比

新旧虚拟DOM对比的时候,Diff算法比较只会在同层级进行, 不会跨层级比较。 所以Diff算法是:深度优先算法。 时间复杂度:O(n)

Diff对比流程

当数据改变时,会触发setter,并且通过Dep.notify去通知所有订阅者Watcher,订阅者们就会调用patch方法,给真实DOM打补丁,从而更新相应的视图。

patch方法

这个方法作用就是,对比当前同层的虚拟节点是否为同一种类型的标签(同一类型的标准,下面会讲)

  • 是:继续执行patchVnode方法进行深层比对
  • 否:没必要比对了,直接整个节点替换成新虚拟节点

来看看patch的核心原理代码

function patch(oldVnode, newVnode) {// 比较是否为一个类型的节点if (sameVnode(oldVnode, newVnode)) {// 是:继续进行深层比较patchVnode(oldVnode, newVnode)} else {// 否const oldEl = oldVnode.el // 旧虚拟节点的真实DOM节点const parentEle = api.parentNode(oldEl) // 获取父节点createEle(newVnode) // 创建新虚拟节点对应的真实DOM节点if (parentEle !== null) {api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素api.removeChild(parentEle, oldVnode.el)  // 移除以前的旧元素节点// 设置null,释放内存oldVnode = null}}return newVnode
}

sameVnode方法

patch关键的一步就是sameVnode方法判断是否为同一类型节点,那问题来了,怎么才算是同一类型节点呢?这个类型的标准是什么呢?

sameVnode方法的核心原理代码

function sameVnode(oldVnode, newVnode) {return (oldVnode.key === newVnode.key && // key值是否一样oldVnode.tagName === newVnode.tagName && // 标签名是否一样oldVnode.isComment === newVnode.isComment && // 是否都为注释节点isDef(oldVnode.data) === isDef(newVnode.data) && // 是否都定义了datasameInputType(oldVnode, newVnode) // 当标签为input时,type必须是否相同)
}

patchVnode方法

这个函数做了以下事情:

  • 找到对应的真实DOM,称为el
  • 判断newVnodeoldVnode是否指向同一个对象,如果是,那么直接return
  • 如果他们都有文本节点并且不相等,那么将el的文本节点设置为newVnode的文本节点。
  • 如果oldVnode有子节点而newVnode没有,则删除el的子节点
  • 如果oldVnode没有子节点而newVnode有,则将newVnode的子节点真实化之后添加到el
  • 如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要
function patchVnode(oldVnode, newVnode) {const el = newVnode.el = oldVnode.el // 获取真实DOM对象// 获取新旧虚拟节点的子节点数组const oldCh = oldVnode.children, newCh = newVnode.children// 如果新旧虚拟节点是同一个对象,则终止if (oldVnode === newVnode) return// 如果新旧虚拟节点是文本节点,且文本不一样if (oldVnode.text !== null && newVnode.text !== null && oldVnode.text !== newVnode.text) {// 则直接将真实DOM中文本更新为新虚拟节点的文本api.setTextContent(el, newVnode.text)} else {// 否则if (oldCh && newCh && oldCh !== newCh) {// 新旧虚拟节点都有子节点,且子节点不一样// 对比子节点,并更新updateChildren(el, oldCh, newCh)} else if (newCh) {// 新虚拟节点有子节点,旧虚拟节点没有// 创建新虚拟节点的子节点,并更新到真实DOM上去createEle(newVnode)} else if (oldCh) {// 旧虚拟节点有子节点,新虚拟节点没有//直接删除真实DOM里对应的子节点api.removeChild(el)}}
}

updateChildren方法

这是patchVnode里最重要的一个方法,新旧虚拟节点的子节点对比,就是发生在updateChildren方法中,接下来就结合一些图来讲,更好理解

是怎么样一个对比方法呢?就是首尾指针法,新的子节点集合和旧的子节点集合,各有首尾两个指针:

源码地址:src/core/vdom/patch.js -404行

这个是新的 vnode 和 oldVnode 都有子节点,且子节点不一样的时候进行对比子节点的函数

  • 然后会进行互相进行比较,总共有五种比较情况:

    • 1、oldS 和 newS使用sameVnode方法进行比较,sameVnode(oldS, newS)
    • 2、oldS 和 newE使用sameVnode方法进行比较,sameVnode(oldS, newE)
    • 3、oldE 和 newS使用sameVnode方法进行比较,sameVnode(oldE, newS)
    • 4、oldE 和 newE使用sameVnode方法进行比较,sameVnode(oldE, newE)
    • 5、如果以上逻辑都匹配不到,再把所有旧子节点的 key 做一个映射到旧节点下标的 key -> index 表,然后用新 vnodekey 去找出在旧节点中可以复用的位置。

v-for 必须要写 key,而且不建议开发中使用数组的 index 作为 key 的原因

https://cn.vuejs.org/v2/api/#key

  • key 的作用主要是为了更高效的更新虚拟 DOM,因为它可以非常精确的找到相同节点,因此 patch 过程会非常高效

  • Vue 在 patch 过程中会判断两个节点是不是相同节点时,key 是一个必要条件。比如渲染列表时,如果不写 key,Vue 在比较的时候,就可能会导致频繁更新元素,使整个 patch 过程比较低效,影响性能

  • 应该避免使用数组下标作为 key,因为 key 值不是唯一的话可能会导致一些意想不到的 bug,使 Vue 无法区分它他,还有比如在使用相同标签元素过渡切换的时候,就会导致只替换其内部属性而不会触发过渡效果

  • 从源码里可以知道,Vue 判断两个节点是否相同时主要判断两者的元素类型和 key 等,如果不设置 key,就可能永远认为这两个是相同节点,只能去做更新操作,就造成大量不必要的 DOM 更新操作,明显是不可取的

    <ul>                      <ul>    <li key="0">a</li>        <li key="0">ff</li>    <li key="1">b</li>        <li key="1">a</li>    <li key="2">c</li>        <li key="2">b</li>                              <li key="3">c</li></ul>                     </ul>
    

    按理说,最理想的结果是:只插入一个li标签新节点,其他都不动,确保操作DOM效率最高。但是我们这里用了index来当key的话

diff在Vue3 中的优化

本文源码版本是 Vue2 的,在 Vue3 里整个重写了 Diff 算法这一块东西,所以源码的话可以说基本是完全不一样的,但是要做的事还是一样的

尤公布的数据就是 update 性能提升了 1.3~2 倍ssr 性能提升了 2~3 倍

  • 事件缓存:将事件缓存,可以理解为变成静态的了

  • 添加静态标记:Vue2 是全量 Diff,Vue3 是静态标记 + 非全量 Diff

  • 静态提升:创建静态节点时保存,后续直接复用

  • 使用最长递增子序列优化了对比流程:Vue2 里在 updateChildren() 函数里对比变更,在 Vue3 里这一块的逻辑主要在 patchKeyedChildren() 函数里,

  • patchKeyedChildren

    在 Vue2 里 updateChildren 会进行

    • 头和头比
    • 尾和尾比
    • 头和尾比
    • 尾和头比
    • 都没有命中的对比

    在 Vue3 里 patchKeyedChildren

    • 头和头比
    • 尾和尾比
    • 基于最长递增子序列进行移动/添加/删除

    看个例子,比如

    • 老的 children:[ a, b, c, d, e, f, g ]
    • 新的 children:[ a, b, f, c, d, e, h, g ]
    1. 先进行头和头比,发现不同就结束循环,得到 [ a, b ]
    2. 再进行尾和尾比,发现不同就结束循环,得到 [ g ]
    3. 再保存没有比较过的节点 [ f, c, d, e, h ],并通过 newIndexToOldIndexMap 拿到在数组里对应的下标,生成数组 [ 5, 2, 3, 4, -1 ]-1 是老数组里没有的就说明是新增
    4. 然后再拿取出数组里的最长递增子序列,也就是 [ 2, 3, 4 ] 对应的节点 [ c, d, e ]
    5. 然后只需要把其他剩余的节点,基于 [ c, d, e ] 的位置进行移动/新增/删除就可以了

    使用最长递增子序列可以最大程度的减少 DOM 的移动,达到最少的 DOM 操作,有兴趣的话去 leet-code 第300题(最长递增子序列) 体验下

探秘vue核心之虚拟DOM与diff算法相关推荐

  1. Vue深入学习—虚拟DOM和Diff算法

    1.snabbdom 是什么? snabbdom是"速度"的意思,源码只有200行,使用TS写的,让东西变得模块化 2.snabbdom 的 h 函数如何工作? h函数用于产生虚拟 ...

  2. vue之购物车案例升级版、v-model之lazy、number、trim的使用、fetch和axios、计算属性、Mixins、虚拟dom与diff算法 key的作用及组件化开发

    文章目录 1.购物车案例升级版(含价格统计.全选/反选.商品增加减少) 2.v-model之lazy.number.trim的使用 3.fetch和axios 3.1.通过jquery+ajax实现v ...

  3. vue 虚拟dom和diff算法详解

    虚拟dom是当前前端最流行的两个框架(vue和react)都用到的一种技术,都说他能帮助vue和react提升渲染性能,提升用户体验.那么今天我们来详细看看虚拟dom到底是个什么鬼 虚拟dom的定义与 ...

  4. 面试准备—vue核心之虚拟DOM(vdom)

    vue核心之虚拟DOM 一.真实DOM和其解析流程? 二.JS操作真实DOM的代价! 三.为什么需要虚拟DOM,它有什么好处? 四.实现虚拟DOM 一.真实DOM和其解析流程? 浏览器渲染引擎工作流程 ...

  5. 【Vue源码解析】Vue虚拟dom和diff算法

    Vue虚拟dom和diff算法 1. 简介 2. 搭建环境 1. 安装snabbdom 2. 安装webpack5并配置 3.函数 3.1 虚拟节点vnode的属性 3.2 使用h函数 创建虚拟节点 ...

  6. 虚拟DOM和Diff算法 - 入门级

    什么是虚拟Dom 我们知道我们平时的页面都是有很多Dom组成,那虚拟Dom(virtual dom)到底是什么,简单来讲,就是将真实的dom节点用JavaScript来模拟出来,而Dom变化的对比,放 ...

  7. vue--mixin混入以及虚拟DOM和diff算法

    mixin混入 使用它的好处: 将 options 中的配置项可以单独抽离出来,单独管理,这样方便维护 使用: 新建一个对象用来保存 options 中某一个配置项,比如: methods 接下来要将 ...

  8. 【总结】1135- 图解虚拟 DOM 之 DIff 算法

    原文: https://juejin.cn/post/7000266544181674014 1. 目录 1. 相关知识点: 2. 虚拟DOM(Virtual DOM) 2.1. 什么是虚拟DOM 2 ...

  9. 浏览器性能优化(2)React 虚拟 dom与diff算法

    随着前端技术快速发展,现在的mvvm几大框架遍布前端行业,那么它们对浏览器的性能到底影响多大?与传统的jq相比做了哪些优化呢? 文章目录: React中的虚拟DOM是什么? 虚拟DOM的简单实现(di ...

最新文章

  1. DHCP服务器的配置详细说明
  2. T-SQL基础(三)之子查询与表表达式
  3. 开学了,也要开始找工作了
  4. redis 内存溢出_查漏补缺,Redis为什么会这么快,看完这七点你就知道了
  5. 远程登陆时,页面登陆不了,提示“user profile serveice服务未能登陆”
  6. (转)淘淘商城系列——分布式文件系统FastDFS
  7. 计算机平面设计论文范,计算机平面设计论文关于计算机平面设计中汉字艺术论文范文参考资料...
  8. linux mysql 5.7.13 安装_mysql 5.7.13 安装配置方法图文教程(linux)
  9. iris鸢尾花数据集java_鸢尾花数据集(Iris)
  10. BigDecimal的round模式
  11. 【情暖寒冬 让爱同行】中创算力开展“寒冬送温暖”公益活动
  12. Vue2基础篇-21-非单文件组件
  13. 启动RabbitMQ成功但是访问localhost:15672无法访问解决方案
  14. 十大电商颓废的背后,我们该思考什么?
  15. excel表格右上角添加小红三角标记
  16. 用AES加密密钥长度报错问题
  17. 让VMware闹心,Nutanix和红帽联手了
  18. k8s 各种类型的Service讲解,及Ingress代理
  19. Android大疆无人机对接大牛直播sdk视频H.264码推流
  20. 微信小程序demo:汇汇生活:电商模板,仿淘宝密码输入框

热门文章

  1. python assert使用
  2. Java –如何延迟几秒钟
  3. 固态硬盘装win7系统(win8、win10基本同理唯一不同就是程序用的安装镜像不同)
  4. C# .NET 遍历Json 形成键值对 取节点值key value
  5. android音乐播放器开发在线加载歌词,android自定义view面试
  6. python编写小游戏17_十分钟教你学会python编写小游戏
  7. 关于FOB/CIF/CNF的报价
  8. VUE_关于Vue.use()详解
  9. 应用宝发布apk问题
  10. Low CP Rank and Tucker Rank Tensor Completion for Estimating Missing Components in Image Data论文笔记