虚拟DOM
在React、Vue两个前端框架中,都使用了虚拟DOM的概念,虚拟DOM是什么呢?其实它的本质是JavaScript对象,用来模拟真实的DOM节点。
虚拟 DOM 节点目前是一个规范化的数据结构,类似如下所示:
1 | { |
使用这个数据结构,最终可以转化为 DOM 节点,等价于如下所示 DOM 元素:
1 | <div style="background-color: blue"> |
为什么要使用虚拟DOM
- 我们都知道,前端性能优化的一个秘诀就是尽可能地减少DOM操作,不仅仅是因为DOM操作的性能差,更是因为频繁的变动DOM会造成浏览器的回流与重绘,因此当我们加了一层虚拟DOM之后,通过对比前后的变化,再更新到真实DOM上,就能有效的减少操作真实DOM的次数。
- 省略手动DOM操作,可以提升开发效率
- 可以更好的跨平台
- 浏览器平台渲染DOM
- 服务端渲染SSR(Nuxt.js/Next.js),前端是Vue向的,后者是React向
- 原生应用(Weex/React Native)
- 小程序(mpvue/uni-app)等
缺点
- 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。虚拟DOM需要在内存中维护一份DOM的副本。
- 如果你的场景是虚拟DOM大量更改,这是合适的。但是单一的、频繁的更新的话,虚拟DOM将会花费更多的时间处理计算的工作。比如,你有一个DOM节点相对较少的页面,用虚拟DOM,它实际上有可能会更慢。但是对于大多数单页应用,都会更快。这也是为啥React和Vue中的更新用了异步的方法,频繁更新时,只更新最后一次的。
Tree Diff
Vue2 算法 —— snabbdom.js
Vue2的Diff实现主要参考的是snabbdom。
这是snabbdom中vnode的定义:
1 | export interface VNodeData { |
这里我们跳过创建vnode的过程,直接看它的核心patch过程。
patch
patch函数主要是做一个中继站,将新旧节点相同的情况与新旧节点不同的情况划分为两种处理:
源码:
1 | return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { |
补充1:sameVnode函数
1 | function sameVnode(vnode1: VNode, vnode2: VNode): boolean { // 通过key和sel选择器判断是否是相同节点 |
patchNode
patchNode函数主要处理两个相同节点的text
与children
的对比。
源码:
1 | function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { |
updateChildren (划重点)
这个部分的函数可以分为三个部分,部分1:声明变量,部分2:同级节点比较(四种命中查找,Diff优化策略),部分3:循环结束的收尾工作。
同级别节点比较的五种情况:
oldStartVnode/newStartVnode
(旧开始节点/新开始节点)相同oldEndVnode/newEndVnode
(旧结束节点/新结束节点)相同oldStartVnode/newEndVnode
(旧开始节点/新结束节点)相同oldEndVnode/newStartVnode
(旧结束节点/新开始节点)相同- 特殊情况当1、2、3、4的情况都不符合的时候就会执行,在
oldVnodes
里面寻找跟newStartVnode
一样的节点然后位移到oldStartVnode
,若没有找到在就oldStartVnode
创建一个
我们来一一探究每种情况的实现方式:
▼情况1:旧开始节点与新开始节点相同
若符合情况1:
- 执行
patchVnode
更新节点 oldStartIdx++/newStartIdx++
▼情况2:旧结束节点与新结束节点相同
若符合情况2:
- 执行
patchVnode
更新节点 oldEndIdx--/newEndIdx--
▼情况3:旧开始节点与新结束节点相同
- 执行
patchVnode
更新节点 oldCh[oldStartIdx]对应的真实dom
位移到oldCh[oldEndIdx]对应的真实dom
后oldStartIdx++/newEndIdx--
▼情况4:旧结束节点与新开始节点相同
- 执行
patchVnode
更新节点 oldCh[oldEndIdx]对应的真实dom
位移到oldCh[oldStartIdx]对应的真实dom
前oldEndIdx--/newStartIdx++
▼情况5:
- 遍历
oldCh
找到与newStartNode
相同的节点 - 当可以找到相同节点时,将相同的节点插入到
oldStartNode
前 - 当无法找到相同节点时,在
oldStartNode
前新增一个新节点
1 | function updateChildren( |