前言
1. 当数据发生变化时,react和 Vue 是怎么更新View的?
渲染真实DOM节点的开销是非常大的,比如有时候我们修改了某个数据,渲染到真实的DOM树上会引起整个DOM 树的重绘和重排,有没有方案只更新我们修改的那一部分而不更新整个DOM 呢? diff 算法就可以实现。
我们先根据真实DOM 生成一颗virtual DOM , 当 Virtual DOM 某个节点的数据发生变化后,会生成一个新的 newVnode, 然后 newVnode 和 oldVnode 进行比较,发现有不一样的地方就直接修改在真实 DOM上,然后使oldVnode 的值更新成 newVnode。
vdom 因为是纯粹的JS对象,所以操作它会很高效,但是vdom 的变更最终会转换成DOM操作,为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要。
2. Virtual DOM 和真实 DOM 的区别?
Virtual DOM 是将真实的Dom 的数据抽取出来,以对象的形式模拟树形结构,diff算法比较的也是virtual Dom 比如 DOM是这样的:
<div>
<span>content</span>
</div>
对应的 Virtual DOM
var vnode = {
tag: '<div>',
children: [
{ tag: 'span', text: 'content'}
],
}
1. Diff算法(差异查找法)
Diff 算法的作用
Diff 算法的作用是用来计算出 Virtual DOM 中被改变的部分,然后针对该部分进行原生 DOM 操作,而不用重新渲染整个页面。
diff 算法是调和的具体实现。 调和 是将 Virtual DOM 树转换成 actual DOM 树的最小操作单过程,称为调和。
什么是 Diff 算法
把树形结构按照层级分解,只比较同级元素。给列表结构的每个单元添加唯一的 key 属性,方便比较。
传统 Diff 算法
diff 算法即差异查找算法; 计算一颗树形结构转换为另一课树行结构需要最少步骤,如果使用传统的diff 算法通过循环递归遍历节点进行对比,其复杂度要达到O(n^3),其中 n 是节点总数,效率十分低下,加入我们要展示1000个节点,那么我们要一次执行上十亿次的比较。
let result = [];
// 比较叶子节点
const diffLeafs = funnction(beforeLeaf, afterLeaf){
// 获取较大节点数的长度
let count = Math.max(beforeLeaf.children.length, afterLeaf.children.length);
for (let i = 0; i < count; i ++) {
const beforeTag = beforeLeaf.children[i];
const afterTag = afterLeaf.children[i];
if (beforeTag === undefined) {
// 添加 afterTag 节点
result.push({ type: 'add', element: afterTag });
} else if (afterTag === undefined) {
// 删除 beforeTag 节点
result.push({ type: 'remove', element: beforeTag });
} else if (beforeTag.tagName !== afterTag.tagName) {
// 节点名发生变化时, 删除before节点,添加after节点
result.push({ type: 'remove', element: beforeTag });
result.push({ type: 'add', element: afterTag });
} else if (beforeTag.innerHTML !== afterTag.innerHTML) {
// 节点名不变而内容改变时,改变节点
if (beforeTag.children.length === 0) {
result.push({
type: 'changed',
beforeElement: beforeTag,
afterElement: afterTag,
html: afterTag.innerHTML
});
} else {
// 递归比较
diffLeafs(beforeTag, afterTag);
}
}
}
return result;
}
diff 三大策略
- 1 策略一、Tree Diff
Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
- 2 策略二、Component Diff
拥有相同类的两个组件将会生成相似的树形结构 。拥有不同类的两个组件将会生成不同的树形结构。
- 3 策略三、Element Diff
对于同一层级的一组子节点,通过唯一 id 进行区分。
1.2. tree diff
(1) React 通过 updateDepth 对 Virtual DOM 树进行层级控制。
(2) 对树分层比较,两棵树 只对同一层次节点进行比较。如果该节点不存在时,则该节点及其节点会被完全删除,不会再进行比较。
(3) 只需遍历一次,就能完成整颗 DOM 树的比较。

如果 DOM 节点出现了跨层级操作,diff 会怎么办?
答: diff 只简单考虑同层级的节点位置变换,如果是跨层级的话,只有创建节点和删除节点的操作。

如上图所示,以A为根节点的整棵树会被重新创建,而不是移动,因此官方建议不要进行DOM节点跨层级操作,可以通过CSS隐藏、显示节点,而不是真正的移除、添加DOM节点。
2. Vue Diff算法
Vue的核心是双向绑定和虚拟DOM,vdom 是树状结构,其节点为 vnode, vnode 和浏览器DOM中的node一一对应,通过Vnode的elm 属性可以访问到对应的Node。
概念: diff 算法是一种优化手段,将前后两个模块进行差异化对比,修补差异的过程叫做patch(打补丁)。
vue中列表循环需加:key="唯一标识"
唯一标识可以是 item 里面的id、index 等具有唯一性字段值,因为Vue组件高度复用增加Key可以标识组件的唯一性,那么key是如何更搞笑的更新虚拟DOM的呢?
- 实例:
我们希望在BC之间加一个 F, diff算法默认执行起来是这样的,即把C更新成F、D更新成C、E 更新成D,最后插入E,这样效率很差。
所以我们需要使用Key 来给每个节点做一个唯一标识,Diff 算法就可以正确的识别次节点,找到正确的位置区插入新的节点。

Vue 通过一系列的措施提升diff 的性能:
1. 优先处理特殊场景
(1) 头部的同类型节点、尾部的同类型节点
这类节点更新前后位置没有发生变化,所以不用移动它们对应的DOM。
(2) 头尾/尾头的同类型节点
这类节点位置很明确,不需要花心思查找,直接移动DOM就好了。
处理了这些场景之后,一方面一些不需要做移动的DOM得到了快速处理,另一方面待处理节点变少,缩小了后续操作的处理范围,性能也得到了提升。
2. 原地复用
原地复用是指Vue会尽可能复用DOM,尽可能不发生DOM的移动。
Vue在判断更前前后指针是否指向同一个节点,其实不要求它们真实引用同一个DOM节点,实际上它仅判断指向的是否是同类型,如果是同类节点,那么Vue会直接复用DOM,这样的好处是不需要移动DOM。
3. React Diff算法
只有在 React 更新阶段才会有 Diff 算法的运用。
- React 不可能采用传统算法。
React 的 Diff 算法。
- React 采用虚拟 DOM 技术实现对真实 DOM 的映射,即 React Diff 算法的差异查找实质是对两个 JavaScript 对象的差异查找。
- 基于三个策略:[Diff 算法的三大策略] 将 O(n^3)复杂度转换成 O(n)复杂度
(1)调和 将 Virtual DOM 树转换成 actual DOM 树的最少操作的过程称为调和。
(2)React Diff 算法,Diff 算法是调和的具体实现。
element diff
当节点处于同一层级时,diff算法提供三种节点操作:删除、插入、移动
情景一: 新旧集合中存在相同节点但位置不同时,如何移动节点:

(1)看着上图的 B,React先从新中取得B,然后判断旧中是否存在相同节点B,当发现存在节点B后,就去判断是否移动B。 B在旧 中的index=1,它的lastIndex=0,不满足 index < lastIndex 的条件,因此 B 不做移动操作。此时,一个操作是,lastIndex=(index,lastIndex)中的较大数=1.
注意:lastIndex有点像浮标,或者说是一个map的索引,一开始默认值是0,它会与map中的元素进行比较,比较完后,会改变自己的值的(取index和lastIndex的较大数)。
(2)看着 A,A在旧的index=0,此时的lastIndex=1(因为先前与新的B比较过了),满足index<lastIndex,因此,对A进行移动操作,此时lastIndex=max(index,lastIndex)=1。
(3)看着D,同(1),不移动,由于D在旧的index=3,比较时,lastIndex=1,所以改变lastIndex=max(index,lastIndex)=3
(4)看着C,同(2),移动,C在旧的index=2,满足index<lastIndex(lastIndex=3),所以移动。
由于C已经是最后一个节点,所以diff操作结束。