1. JSX与虚拟DOM
我们从React官方文档开头最基本的一段Hello World代码入手:
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
这段代码的意思是通过 ReactDOM.render()
方法将 h1
包裹的JSX元素渲染到id为“root”的HTML元素上. 除了在JS中早已熟知的 document.getElementById()
方法外, 这段代码中还包含两个知识点:
- 以
h1
标签包裹的JSX元素 ReactDOM.render()
方法
而这两个知识点则对应着React中要解决的核心问题:
- 为何以及如何使用(JSX表示的)虚拟DOM?
- 如何对虚拟DOM进行处理, 使其高效地渲染出来?
1.1 虚拟DOM是什么? 为何要使用虚拟DOM?
虚拟DOM其实就是用JavaScript对象表示的一个DOM节点, 内部包含了节点的 tag
, props
和 children
.
为何使用虚拟DOM? 因为直接操作真实DOM繁琐且低效, 通过虚拟DOM, 将一部分昂贵的浏览器重绘工作转移到相对廉价的存储和计算资源上.
1.2 如何将JSX转换成虚拟DOM?
React实战视频讲解:进入学习
通过babel可以将JSX编译为特定的JavaScript对象, 示例代码如下:
// JSX
const e = (
<div id="root">
<h1 className="title">Title</h1>
</div>
);
// babel编译结果(React17之前), 注意子元素的嵌套结构
var e = React.createElement(
"div",
{ id: "root"},
React.createElement(
"h1",
{ className: "title" },
"Title"
)
);
// React17之后编译结果有所区别, 创建节点的方法由react导出, 但基本原理大同小异
1.3 如何将虚拟DOM渲染出来?
从上一节babel的编译结果可以看出, 虚拟DOM中包含了创建DOM所需的各种信息, 对于首次渲染, 直接依照这些信息创建DOM节点即可.
但虚拟DOM的真正价值在于“更新”: 当一个list中的某些项发生了变化, 或删除或增加了若干项, 如何通过对比前后的虚拟DOM树, 最小化地更新真实DOM? 这就是React的核心目标.
2. React Diffing
"Diffing"即“找不同”, 就是解决上文引出的React的核心目标——如何通过对比新旧虚拟DOM树, 以在最小的操作次数下将旧DOM树转换为新DOM树.
在算法领域中, 两棵树的转换目前最优的算法复杂度为 O(n**3)
, n为节点个数. 这意味着当树上有1000个元素时, 需要10亿次比较, 显然远远不够高效.
React在基于以下两个假设的基础上, 提出了一套复杂度为 O(n)
的启发式算法
- 不同类型(即标签名、组件名)的元素会产生不同的树;
- 通过设置
key
属性来标识一组同级子元素在渲染前后是否保持不变.
在实践中, 以上两个假设在绝大多数场景下都成立.
2.1 Diffling算法描述
不同类型的元素/组件
当元素的标签或组件名发生变化, 直接卸载并替换以此元素作为根节点的整个子树.
同一类型的元素
当元素的标签相同时, React保留此DOM节点, 仅对比和更新有改变的属性, 如className、title等, 然后递归对比其子节点.
对于 style
属性, React会继续深入对比, 仅更新有改变的属性, 如color、fontSize等.
同一类型的组件
当组件的props更新时, 组件实例保持不变, React调用组件的 componentWillReceiveProps()
componentWillUpdate()
和 componentDidUpdate()
生命周期方法, 并执行 render()
方法.
Diffing算法会递归比对新旧 render()
执行的结果.
对子节点的递归
当一组同级子节点(列表)的末尾添加了新的子节点时, 上述Diffing算法的开销较小; 但当新元素被插入到列表开头时, Diffing算法只能按顺序依次比对并重建从新元素开始的后续所有子节点, 造成极大的开销浪费.
解决方案是为一组列表项添加 key
属性, 这样React就可以方便地比对出插入或删除项了.
关于 key
属性, 应稳定、可预测且在列表内唯一(无需全局唯一), 如果数据有ID的话直接使用此ID作