探秘vue核心之虚拟DOM与diff算法
探秘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
的代价? 用我们传统的开发模式,原生 JS
或 JQ
操作 DOM
时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。在一次操作中,我需要更新 10 个 DOM
节点,浏览器收到第一个 DOM
请求后并不知道还有 9 次更新操作,因此会马上执行流程,最终执行10 次。例如,第一次计算完,紧接着下一个 DOM
更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算 DOM
节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作 DOM
的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验
虚拟 DOM
的好处
虚拟 DOM
就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有 10 次更新 DOM
的动作,虚拟 DOM
不会立即操作 DOM
,而是将这 10 次更新的 diff
内容保存到本地一个 JS
对象中,最终将这个 JS
对象一次性 attch
到 DOM
树上,再进行后续操作,避免大量无谓的计算量。所以,用 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
- 判断
newVnode
和oldVnode
是否指向同一个对象,如果是,那么直接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
表,然后用新vnode
的key
去找出在旧节点中可以复用的位置。
- 1、
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 ]
- 先进行头和头比,发现不同就结束循环,得到
[ a, b ]
- 再进行尾和尾比,发现不同就结束循环,得到
[ g ]
- 再保存没有比较过的节点
[ f, c, d, e, h ]
,并通过 newIndexToOldIndexMap 拿到在数组里对应的下标,生成数组[ 5, 2, 3, 4, -1 ]
,-1
是老数组里没有的就说明是新增 - 然后再拿取出数组里的最长递增子序列,也就是
[ 2, 3, 4 ]
对应的节点[ c, d, e ]
- 然后只需要把其他剩余的节点,基于
[ c, d, e ]
的位置进行移动/新增/删除就可以了
使用最长递增子序列可以最大程度的减少 DOM 的移动,达到最少的 DOM 操作,有兴趣的话去 leet-code 第300题(最长递增子序列) 体验下
探秘vue核心之虚拟DOM与diff算法相关推荐
- Vue深入学习—虚拟DOM和Diff算法
1.snabbdom 是什么? snabbdom是"速度"的意思,源码只有200行,使用TS写的,让东西变得模块化 2.snabbdom 的 h 函数如何工作? h函数用于产生虚拟 ...
- 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 ...
- vue 虚拟dom和diff算法详解
虚拟dom是当前前端最流行的两个框架(vue和react)都用到的一种技术,都说他能帮助vue和react提升渲染性能,提升用户体验.那么今天我们来详细看看虚拟dom到底是个什么鬼 虚拟dom的定义与 ...
- 面试准备—vue核心之虚拟DOM(vdom)
vue核心之虚拟DOM 一.真实DOM和其解析流程? 二.JS操作真实DOM的代价! 三.为什么需要虚拟DOM,它有什么好处? 四.实现虚拟DOM 一.真实DOM和其解析流程? 浏览器渲染引擎工作流程 ...
- 【Vue源码解析】Vue虚拟dom和diff算法
Vue虚拟dom和diff算法 1. 简介 2. 搭建环境 1. 安装snabbdom 2. 安装webpack5并配置 3.函数 3.1 虚拟节点vnode的属性 3.2 使用h函数 创建虚拟节点 ...
- 虚拟DOM和Diff算法 - 入门级
什么是虚拟Dom 我们知道我们平时的页面都是有很多Dom组成,那虚拟Dom(virtual dom)到底是什么,简单来讲,就是将真实的dom节点用JavaScript来模拟出来,而Dom变化的对比,放 ...
- vue--mixin混入以及虚拟DOM和diff算法
mixin混入 使用它的好处: 将 options 中的配置项可以单独抽离出来,单独管理,这样方便维护 使用: 新建一个对象用来保存 options 中某一个配置项,比如: methods 接下来要将 ...
- 【总结】1135- 图解虚拟 DOM 之 DIff 算法
原文: https://juejin.cn/post/7000266544181674014 1. 目录 1. 相关知识点: 2. 虚拟DOM(Virtual DOM) 2.1. 什么是虚拟DOM 2 ...
- 浏览器性能优化(2)React 虚拟 dom与diff算法
随着前端技术快速发展,现在的mvvm几大框架遍布前端行业,那么它们对浏览器的性能到底影响多大?与传统的jq相比做了哪些优化呢? 文章目录: React中的虚拟DOM是什么? 虚拟DOM的简单实现(di ...
最新文章
- DHCP服务器的配置详细说明
- T-SQL基础(三)之子查询与表表达式
- 开学了,也要开始找工作了
- redis 内存溢出_查漏补缺,Redis为什么会这么快,看完这七点你就知道了
- 远程登陆时,页面登陆不了,提示“user profile serveice服务未能登陆”
- (转)淘淘商城系列——分布式文件系统FastDFS
- 计算机平面设计论文范,计算机平面设计论文关于计算机平面设计中汉字艺术论文范文参考资料...
- linux mysql 5.7.13 安装_mysql 5.7.13 安装配置方法图文教程(linux)
- iris鸢尾花数据集java_鸢尾花数据集(Iris)
- BigDecimal的round模式
- 【情暖寒冬 让爱同行】中创算力开展“寒冬送温暖”公益活动
- Vue2基础篇-21-非单文件组件
- 启动RabbitMQ成功但是访问localhost:15672无法访问解决方案
- 十大电商颓废的背后,我们该思考什么?
- excel表格右上角添加小红三角标记
- 用AES加密密钥长度报错问题
- 让VMware闹心,Nutanix和红帽联手了
- k8s 各种类型的Service讲解,及Ingress代理
- Android大疆无人机对接大牛直播sdk视频H.264码推流
- 微信小程序demo:汇汇生活:电商模板,仿淘宝密码输入框
热门文章
- python assert使用
- Java –如何延迟几秒钟
- 固态硬盘装win7系统(win8、win10基本同理唯一不同就是程序用的安装镜像不同)
- C# .NET 遍历Json 形成键值对 取节点值key value
- android音乐播放器开发在线加载歌词,android自定义view面试
- python编写小游戏17_十分钟教你学会python编写小游戏
- 关于FOB/CIF/CNF的报价
- VUE_关于Vue.use()详解
- 应用宝发布apk问题
- Low CP Rank and Tucker Rank Tensor Completion for Estimating Missing Components in Image Data论文笔记