这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战
TIP 👉 登山则情满于山,观海则意溢于海——刘勰
前言
本文会在教你理解基本的 Virtual DOM 算法,并且尝试尽量把 Virtual DOM 的算法思路阐述清楚。希望在阅读本文后,能让你理解 Virtual DOM,给你现有前端的编程提供一些新的思考。DOM操作演化史
早期时期
在前端这个工种的萌芽阶段,JavaScript 在很长一段时间里都不是前端世界的主角,人们只用 JS 来做一些类似于拖拽、隐藏这样简单的动效。这个时期里,前端工程师需要关心的 DOM 操作是有限的。这样看来,使用 JS、jQuery 来定点对 DOM 进行修改好像也不是什么特别让人头大的事情。
模版时期
随着前端业务复杂度不断提升,前端页面对交互体验的要求越来越高,骤增的动态内容带来了大量的 DOM 修改需求。此时若再要求工程师们去逐一修改 DOM 节点,其工作量将大到令人绝望。于是创造出了“模板”这一解决方案
比如说我有一个学生信息表格需要展示,那么我可以给它一组初始化数据 students:
[
{
name: 'tes1', age: 24
},
{
name: 'Lili', age: 22
},
{
name: 'John', age: 23
}
]
然后把这组数据塞进 template 去:
<table>
{% students.forEach(function(student){ %}
<tr>
<td>{% student.name %}</td>
<td>{% student.age %}</td>
</tr>
{% }); %}
</table>
模板会帮我们做什么呢?它会把你的 students 这个数据源读进去,塞到上面这段 template 代码里,把它们融合在一起,吐出一段目标 HTML 给你。然后这段 HTML 代码就可以直接被拿去渲染到页面上,成为 DOM。
这个过程差不多是这样:
// 数据和模板融合出 HTML 代码
var targetDOM = template({data: students})
// 添加到页面中去
document.body.appendChild(targetDOM)
模板带来的问题
模板这种形式的 DOM 方案,其实是非常粗糙的,蕴含了不小的隐患。
大家现在考虑一个常见的场景:如果我发现上述表格中某个同学的名字写错了——tes1 其实叫test。现在我要把这个名字改掉,于是我改了 students 里对应的姓名信息,模板会做什么呢?
首先,模板引擎会把 targetDOM 这个节点整个给注销掉; 然后,再重新走一遍刚刚走过的渲染流程:
1. 数据+模板=HTML代码
2. 把 HTML 代码渲染到页面上,形成真实的 DOM
本来我只是想改 tes1 的名字,现在整个表格都需要被重新渲染。DOM 操作的范围,从小小的一个表格字段位, 扩大到了整个表格。这不合理。
现代前端框架的基石——虚拟DOM
上面模版实现DOM操作的过程:注销旧DOM -> 数据 + 模板 => 新的一套HTML 代码 -> 挂载新 DOM
这里的“旧DOM”、“新 DOM”指的都是模板对应的整块 DOM 的整体更新。我们错就错在每次都整体更新——如果有一种方法,可以既帮我们保持住模板方案的数据驱动思想,又做到像人肉 JS、jQuery 一样能够定点只对需要修改的 DOM 做小范围操作,那该多好!
DOM 操作从“一刀切”到“精细化”,中间需要的是啥?需要的是 diff !
虚拟 DOM + diff,新的 DOM 操作解决方案应运而生!
其中,虚拟 DOM 这一层是用 JS 实现的。也就是说在这个阶段所有的更改、对比操作都是纯 JS 层面的计算。JS vs DOM操作,其性能消耗完全不在一个量级上。模板渲染带来的性能问题,就这样被 Virtual DOM 完美地解决了。
Diff算法
react Diff算法
React团队没有采纳递归逐个对比两棵树的算法。他们采纳了一种复杂度仅为 O(n)的 算法——现在,100个节点走一次 只需要对比 100 次,对浏览器来说不费吹灰之力。实际上,这个算法也确实是 React 的一大亮点。下面我们就一起来看看这个 O(n)复杂度的算法是如何实现的:
React 团队根据前端界面的特性,作了这样的假设:
相同的组件有着相同的 DOM 结构,不同的组件有着不同的 DOM 结构
位于同一层次的一组子节点,它们之间可以通过唯一的 id 进行区分
DOM 结构中,跨层级的节点操作非常少,可以忽略不计
首先,当我们考虑两棵树的“不同”时,可以一层一层来考虑,也就是“逐层对比”(如下图所示的关系)。
当对比两棵树时,diff 算法会优先比较两棵树的根节点,如果它们的类型不同,比如说之前是 div,现在变成 p了:那么就认为这两棵树完全不同,这是两个完全不同的组件,因此也没有必要再往下再比对子节点了。
若根节点类型相同,React 才会认为“你没变,你还是那个组件”。接下来,在保留这个组件的基础上,检查其属性的变化,然后根据属性变化的情况去更新组件。
处理完根节点这个层次的对比,React 会继续跳到下个层次去对比根节点的子节点们:
子节点的对比思路和根节点是一致的:比如说上面咱们看到 A 变成了 B,那么 React 会认为 A 和 B 的子节点都没有对比的必要了——爹都不是一个,儿子咋可能长一样呢?于是直接从 A 节点开始,把它和相关子节点一起删除重建为 B 及其子节点。
数组动态生成的组件,为什么一定要有 “key” ?
结合咱们前面对 diff 的分析,我们来看这样的两个 Virtual DOM 树:
我们对比第二层节点的过程:
- 对比B和B,没变化,不动
- 对比D和C,节点类型不一样,直接删掉D,重新创建C
- 对比E和D,即诶单类型不一样,直接删掉E,重新创建D
- 对比空和E,E是新增即诶安,新增E
D、E组件都是已存在的子组件,我们如何复用已存在的组件?我们给它打上一个“记号”,这就是key
我们给每个节点打上Key
tips:务必确认key是唯一稳定的。