React 的渲染机制:
- 在
render()
方法中会返回 JSX,JSX 会被转换成React.createElement()
函数的调用。 React.createElement()
会创建出来一个 ReactElement(React 元素),ReactElement 是一个虚拟节点,本质上就是一个 JS 对象。- 一堆的 ReactElement 组成一个 JS 的对象树,也就是虚拟 Dom。
- 最终 React 根据虚拟 Dom 渲染出真实 Dom (
document.createElement()
),呈现在页面中。
// JSX
const element = (
<div className='header'>
<div>标题</div>
</div>
)
// JSX 本质上是 React.createElement()
const element = React.createElement(
"div",
{
className: "header"
},
React.createElement("div", null, "标题")
)
// 虚拟 DOM 节点
console.log(element)
// 真实 DOM 节点
<div class='header'>
<div>标题</div>
</div>
const dom = document.getElementsByClassName('header')[0]
console.log(dom)
React 的更新机制:
React 中的虚拟 Dom:
虚拟节点:
虚拟节点:React 会将 JSX 转换成 React.createElement()
函数调用;React.createElement()
会创建出来一个 ReactElement,React 元素就是一个虚拟节点,本质上就是一个 JS 对象。
虚拟 Dom:
虚拟 Dom:ReactElement 组成的对象树,其实就是由一堆 JS 对象来模拟真实的 Dom 结构。
虚拟 Dom 的作用:
- 通过虚拟 Dom 和 Diff 算法可以实现视图的高效更新:
- 在 React 中,所有的 Dom 构造都是通过虚拟 Dom 进行,而不总是直接操作页面真实 Dom,虚拟 Dom 是内存数据,性能极高。
- 每当数据变化时,React 都会重新构建整个完整的 Dom 树,然后将当前 Dom 树和上一次的 Dom 树进行对比,得到 Dom 结构的区别;然后仅仅将需要变化的部分进行实际的浏览器 Dom更新,而且 React 能够批量处理虚拟 Dom 的刷新。
- 通过虚拟 Dom 可以实现跨平台:虚拟 Dom 本质上就是一堆 JS 对象,因此 React 既可以将它渲染成 Web 端的 HTML 元素,也可以通过桥接的方式将它渲染成移动端(IOS、Android)的控件。
React 中的 Diff 算法:
Diff 算法:通过对比新旧 DOM 树,找出其中的差异,最小化更新视图。
传统的 Diff 算法:
传统的 Diff 算法需要循环对比两棵树,单纯比较次数就是 O(n^2)
,找到差异后再重新进行排序,最终时间复杂度可达到 O(n^3)
。
React 的 Diff 算法:
React 中使用三个层级的策略对传统的 Diff 算法进行了优化,使复杂度从 O(n^3)
降到了 O(n)
。
React 的 Diff 算法采用了深度优先遍历算法。
tree diff:
树层级的比较:两棵树只对同层级的节点进行比较,不考虑节点的跨层级比较。如果同层级相同位置节点不一样,则直接删除旧的创建新的。
只有创建、删除操作、没有移动操作。
React 只会对相同颜色框内的节点进行比较,如此只需要遍历一次虚拟 Dom 树,就可以完成整个的对比。
B 节点发生了跨层级的移动操作,React 并不会复用 B 节点及其子节点,而是会直接删除 A 节点下的 B 节点及其子节点,然后再在 C 节点下创建新的 B 节点及其子节点。
component diff:
组件的比较:如果是同类型的组件(即两个组件是同一个类的两个实例),按照 tree diff 树比对策略进行比较;如果不是同类型的组件,则直接删除旧的创建新的。
只有创建、删除操作、没有移动操作。
对于同类型组件, React 通过让开发人员自定义shouldComponentUpdate()
方法来进行比较优化,减少组件不必要的比较。如果没有自定义,shouldComponentUpdate()
方法默认返回 true,每次组件发生数据(state & props)变化时,都会进行比较。
虽然组件 C 和组件 H 结构相似,但类型不同,React 不会进行比较,会直接删除组件 C,创建组件 H。
element diff:
单个节点的比较:同一层级的单个节点,通过标记 key 的方式来进行对比。
对于同一层级的单个节点,有三种操作,分别是移动、创建和删除,针对是否使用 key 标识可分为两种情况:
- 对于不使用 key 标识的情况:React 对同一层级相同位置的的新旧单个节点逐个进行对比,如果一致则复用,如果不一致则直接删除旧的创建新的。
React 对同一层级的新旧单个节点进行对比,发现新集合中的 B 不等于旧集合中的 A,于是删除 A,创建 B,依此类推,直到删除 D,创建 C。这会使得相同的节点不能复用,出现频繁的删除和创建操作,从而影响性能。 - 对于使用 key 标识的情况:React 会对新集合进行遍历,通过唯一的 key 来判断旧集合中是否存在相同的节点,如果没有则创建,如果有则判断是否需要进行移动操作。
React Fiber:
reconciler:协调器。用来对 VNode 进行 diff。
renderer:渲染器。用来将 VNode 转为真实 DOM 。
为什么需要 Fiber?
React16 之前的 reconciler 是采用递归形式工作的,一旦开始就无法中断,否则就得重来。也就是说,在 setState 后,reconciler 需要将所有的 Virtual DOM 都递归遍历完成后,才能给出当前需要修改的真实 DOM 的信息,给到 renderer 进行渲染。
因此就会导致 React 从 setState 开始到渲染完成,整个过程是同步的,一气呵成。如果需要渲染的组件比较庞大,JS 执行占据主线程的时间就会较长,会导致页面响应度变差。
由于 JS 是单线程的,因此浏览器的 JS 引擎和渲染引擎是互斥的,当其中一个执行时,另一个只能挂起等待。
通常将 React16 之前的 reconciler 称为 stack reconciler,React16 之后的 reconciler 称为 fiber reconciler。
React Fiber 的原理:
因此在 React v16.0 中引入了 React Fiber 架构。Fiber 需要实现两个功能:
-
新的任务调度:将任务拆分成一个个小的任务,有高优先级任务的时候将浏览器让出来先执行高优先级的任务,等浏览器空闲了再继续执行之前的任务。
React Fiber 将 Virtual DOM 的更新拆分成了多个小任务,每个小任务只更新部分组件的 DOM,并给每个任务分配了优先级;利用 requestIdleCallback API 在浏览器的空闲时间去执行小任务,每执行一个任务单元后,查看是否有其他高优先级的任务,如果有的话就先执行优先级高的任务。
requestIdleCallback:当前浏览器处于空闲状态才会执行传入其中的回调函数。
-
新的数据结构:可以随时中断,下次进行可以接着执行。
React Fiber 中是扁平的链表结构,每一个 React 元素对应一个 fiber,每个 fiber 中不仅包含着当前元素的信息,还保存着父节点的地址、第一个子节点的地址、下一个兄弟节点的地址。因此无论在哪个点执行中断,只要将下标存起来就行就能恢复。