文章目录
- [前端八股整理|Vue|虚拟 DOM、Diff 与 Patch 流程](#前端八股整理|Vue|虚拟 DOM、Diff 与 Patch 流程)
- [1.什么是虚拟dom?虚拟 dom 的好处?](#1.什么是虚拟dom?虚拟 dom 的好处?)
- [2. diff算法](#2. diff算法)
-
- [2.1 patch 函数](#2.1 patch 函数)
- [2.2 patchVnode 函数](#2.2 patchVnode 函数)
- [2.3 updateChildren函数](#2.3 updateChildren函数)
前端八股整理|Vue|虚拟 DOM、Diff 与 Patch 流程
1.什么是虚拟dom?虚拟 dom 的好处?
参考视频:Vue中的虚拟DOM 虚拟DOM如何使用?有什么好处?
jQuery时代是操作 Dom->视图更新
vue 时代是数据改变->操作 Dom->视图更新,但是从实际上执行来说 运行 js要比操作 dom 快的多得多,所以引入了虚拟 Dom
数据改变->虚拟 DOM(计算变更)->操作真实的 DOM->视图更新
什么是虚拟Dom?
就是用 JS 模拟 DOM 结构.虚拟 DOM 的三个关键组成是 tag,props,children.其中 tag 就是代表这个 html 的标签是什么,props 里面放一些 id,className 就看标签上有哪些属性,children 就是这个标签里面嵌套了哪些内容.
html 对应的虚拟 Dom
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app" class="container">
<h1>虚拟DOM</h1>
<ul style="color: red;">
<li>第一项</li>
<li>第二项</li>
</ul>
</div>
</body>
</html>
javascript
{
tag:'div',
props:{
id:'app',
className:'container'
},
children:[
{
tag:'h1',
children:'虚拟 DOM'
},
{
tag:'ul',
props:{style="color:red"},
children:[
{
tag:'li',
children:'第一项'
},
{
tag:'li',
children:'第二项'
}
]
}
]
}
虚拟 dom 的好处:
比如更新 dom,虚拟dom 会计算一个最小的更新视图,再去操作 dom,比如有两个 dom 节点,现在修改 dom,第一项不变,第二项修改内容,第三项新增,使用虚拟 dom,最后只会更新第二项和第三项
2. diff算法
理论上,比较两棵树的最小差异是一个 O(n³) 的复杂问题(经典"树编辑距离"问题)。
对前端 DOM 来说,O(n³) 根本不可接受(n=1000 时已经算不动)。
Vue 采用了 启发式策略:只比较同层级节点,不做跨层比较 → 复杂度降低为 O(n)。
只比较同一层级

同层比较时
- 标签名(tag)不同,直接删除,不继续深度比较
- tag 相同但 key 不同:通常也认为不是同一个节点
- 标签名(tag)相同,key相同,可以认为是相同节点,继续比较 props 和 children
虚拟 dom 节点 vnode 是怎么创建出来的?

typescript
return {sel,data,children,text,elm,key}
- sel:选择器,例如 div
- data:比如样式
- children:一个数组,包含了他的子节点
- text:和 children 是对立的,两者只存在一个,text 就是纯文本
- elm:就是要把这个节点渲染到某个真实的 dom 节点上的 dom 节点
- key:v-for 提供的那个 key
2.1 patch 函数
patch 函数
首次页面渲染的时候去执行一次,目的是给他渲染到一个空的容器里来
javascript
patch(container,vnode)
后面页面更新的时候,会把新的 vnode 去替换掉老的 vnode
javascript
patch(vnode,newVnode)
patch 函数的过程
patch(oldVnode,vnode)
- 首先就是如果传入的第一个参数不是 vnode,对应的就是首次渲染传入 container 的情况,此时就会创建空的 vnode,并关联 DOM元素,这样后续更新的时候就知道我们要更新在哪个 dom元素上
- 接下来,判断传入的 oldvnode 和 vnode 是不是同一个节点,比较的就是 key 和sel,如果此时相同,就做更新 patchVnode(oldVnode,vnode)如果不相同,就会创建一个新的 DOM 元素,再插入这个新的 DOM 元素,最后移除老的 DOM 元素
根据新的虚拟 DOM,重新创建一个新的真实 DOM 节点,然后把旧的真实 DOM 替换掉。
2.2 patchVnode 函数
patchVnode 函数的过程
-
首先就是给新的 vnode 挂上老的 vnode 对应的真实 dom 元素,也就是那个 elm,因为新的 vnode 很可能不知道之前要挂在哪个真实的 dom 节点上
-
然后就是比较老的 vnode 和新的 vnode 的 children,如果相同,就直接返回
-
接下来就是很重要的逻辑,children 不同的那些情况都怎么去判断实现
-
新 vnode 无 text
- 新 vnode有 children
- 老的 vnode也有children,此时就要用 updateChildren 做更新
- 老的 vnode 没有children,但是有 text,那么就将真实 dom 元素中的 text清空,将新的 vnode 的 children 添加到真实的 dom 上
- 新 vnode没有 children
- 这种情况应该是一个空节点,如果老的存在就对应的在真实 dom 上删掉即可
- 新 vnode有 children
-
新的 vnode 有 text,没有 children
- 老的 vnode 存在 text,首先做比较,老的 vnode 的 text不等于新的vnode 的 text,此时如果老的 vnode 里面有 children,则删除真实 dom 元素的 children,设置真实的 dom 元素的 text 为新 vnode 的 text
- 老的 vnode 不存在 text,那真实 dom 就更新成新 vnode,老的 vnode 有 children ,真实 DOM 里的 children 清掉,再设置 text
注意:
text 和children 根据 vnode 的定义,两者只会出现一个,是对立的,切记!
-
2.3 updateChildren函数
updateChildren函数的过程:

javascript
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 依次尝试 4 种命中
}
循环的主控条件是两边都没处理完
4 种命中分别是
- 新头比旧头
- 新头比旧尾
- 新尾比旧头
- 新尾比旧尾
如果都没命中,走兜底操作,比较 key 值
为什么要一轮一轮的进行四种命中呢?
因为这是一种很省成本的启发式策略,本质在赌一个事实,前端列表更新里,很多变动其实都发生在头尾附近。比如头部插入,尾部追加等等.优先处理最常见的场景
如果这 4 种方式命中了,命中后先 patchVnode,必要时做 DOM 移动,然后再移动对应指针。那么就是对应的指针++或者--
比如新头和旧尾命中了,那就是 newStartIdx++,oldEndIdx--
如果没命中,那就以新头指针为一个标志,拿着他的 key,去旧的列表中去找可复用的节点,如果没找到就创建新的,如果找到了但是tag 不同,那也创建新的,如果相同,也是继续去调用patchVnode 函数进行比较,
对应的指针 newStartIdx肯定是++,旧列表的对应元素,设置一个记号,被复用的位置标记为空位 / undefined,表示这个位置已经处理过了,后续跳过(在匹配上的情况)