# React
# react fiber是什么
React Fiber 是 React 16 引入的一种新的协调引擎(reconciliation engine),它对 React 的核心算法进行了重写,以支持更高效的调度和渲染。Fiber 架构的主要目标是提升应用的性能,特别是对于大型应用和复杂用户界面,同时提供更好的用户体验,特别是在动画、布局计算和其他耗时操作方面。
主要特点
可中断的渲染:
- 在传统的 React 协调过程中,一旦开始更新 UI,整个过程是同步完成的(深度优先遍历),这意味着在更新过程中浏览器无法响应用户的交互。
- Fiber 引入了可中断的机制,允许 React 在每次执行一小段工作后暂停,让出控制权给浏览器去处理其他任务,比如用户输入或动画帧。
优先级调度:
- Fiber 可以根据任务的紧急程度来分配不同的优先级。例如,用户输入事件可以被赋予更高的优先级,而一些非关键的更新则可以推迟到空闲时间进行。
- 这种机制确保了重要的更新能够快速地反映到界面上,而不重要的更新不会阻塞主线程。
并发模式:
- Fiber 支持并发模式,这是 React 18 中引入的一个重要特性。并发模式允许 React 在同一时刻准备多个版本的 UI,并选择最佳时机进行提交。
- 这对于实现诸如 Suspense for Data Fetching 等高级特性至关重要,因为它允许组件等待数据加载完成后再渲染。
时间切片:
- Fiber 使用了时间切片技术,将一个大的任务分割成多个小的任务片段,每个片段都在浏览器的一帧内完成(通常是 16ms)。
- 如果在一帧内有剩余的时间,React 会继续执行下一个任务片段;如果没有剩余时间,则会等到下一帧再继续执行。
新的生命周期方法:
- 随着 Fiber 的引入,React 推荐使用新的生命周期方法,如
getDerivedStateFromProps
和getSnapshotBeforeUpdate
,并逐步弃用一些旧的方法,如componentWillMount
,componentWillReceiveProps
, 和componentWillUpdate
。
- 随着 Fiber 的引入,React 推荐使用新的生命周期方法,如
Suspense 和错误边界:
- Fiber 支持 Suspense 组件,可以在数据加载期间显示 fallback 内容,从而提供更好的用户体验。
- 错误边界(Error Boundaries)也是基于 Fiber 实现的,它们可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它还会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。
# 工作流程
创建 Fiber 节点:
- 每个虚拟 DOM 节点都有一个对应的 Fiber 节点。
- Fiber 节点包含了节点的信息(如类型、属性等)以及一些额外的数据结构,用于协调和调度。
双缓存树结构:
- Fiber 维护了两棵树:当前显示的树(current tree)和正在构建的工作树(work-in-progress tree, WIP)。
- 当开始一个新的更新时,WIP 树从 current 树克隆而来,并在这个副本上进行更改。
任务调度:
- React 使用浏览器的
requestIdleCallback
或者自定义的时间切片机制来安排任务。 - 在每一帧内,React 会尝试完成尽可能多的任务,如果时间用尽,则保存当前进度并在下一帧继续。
- React 使用浏览器的
协调(Reconciliation):
- 在每个任务中,React 会比较 WIP 树和 current 树之间的差异。
- 如果发现需要更新的地方,React 会在 WIP 树上标记这些变化。
提交阶段:
- 当 WIP 树准备就绪时,React 会进入提交阶段。
- 提交阶段分为三个子阶段:
- beforeMutation:在这一步中,React 会触发
getSnapshotBeforeUpdate
生命周期方法。 - mutation:在这一步中,React 会应用所有的 DOM 变更。
- layout:在这一步中,React 会读取布局信息并触发
componentDidUpdate
生命周期方法。
- beforeMutation:在这一步中,React 会触发
# fiber架构如何中断的
React Fiber 架构通过时间切片(Time Slicing)和优先级调度来实现可中断的工作流程。这意味着 React 可以在执行过程中暂停,让出控制权给浏览器处理其他高优先级的任务,如用户输入或动画更新。以下是 Fiber 如何实现中断的具体机制:
时间切片(Time Slicing)
任务分割:
- 在传统的同步模式下,一旦开始渲染过程,React 会一直执行直到完成整个树的更新。
- 在 Fiber 架构中,渲染工作被分解成一系列小任务,每个任务对应一个 Fiber 节点。
工作循环:
- React 使用
requestIdleCallback
或者自定义的时间管理函数来安排这些小任务。 - 每个任务都会尝试在一帧内(通常为 16ms)完成尽可能多的工作。
- React 使用
剩余时间计算:
- 当浏览器调用回调函数时,它会提供一个截止时间(deadline),表示当前帧还有多少时间可以用来执行非关键任务。
- 如果在这段时间内没有完成所有工作,React 会保存当前状态,并在下一帧继续执行剩下的工作。
暂停与恢复:
- 如果在执行过程中发现剩余时间不足,React 会暂停当前的工作。
- 下一帧到来时,React 会从上次暂停的地方继续执行。
优先级调度
优先级分配:
- 每个更新都有一个优先级,这决定了它们何时被执行。
- 高优先级的更新(例如用户交互)会被立即处理,而低优先级的更新(例如后台数据获取)可能会被推迟。
优先级队列:
- React 维护了一个优先级队列,根据优先级对任务进行排序。
- 在每一帧内,React 会先处理最高优先级的任务。
抢占式调度:
- 如果在处理低优先级任务时有高优先级的任务进入队列,React 会中断当前任务,转而去处理高优先级的任务。
- 一旦高优先级的任务完成,React 会回到之前被中断的位置继续执行。
具体实现细节
Fiber 节点结构:
- 每个 Fiber 节点包含了一些用于协调和调度的信息,比如
child
,sibling
,return
等指针,以及alternate
指针指向当前节点的副本。 - 这些信息帮助 React 在不同阶段之间切换并保存状态。
- 每个 Fiber 节点包含了一些用于协调和调度的信息,比如
双缓存树:
- React 维护了两个 Fiber 树:一个是当前显示的树(current tree),另一个是正在构建的工作树(work-in-progress tree, WIP)。
- 当一个新的更新开始时,WIP 树从 current 树克隆而来,并在这个副本上进行更改。
协调阶段:
- 在协调阶段,React 会比较 WIP 树和 current 树之间的差异,并标记需要更新的部分。
- 如果时间用尽,React 会保存 WIP 树的状态,并在下一帧继续协调。
提交阶段:
- 一旦 WIP 树准备就绪,React 会进入提交阶段,将更改应用到 DOM 上。
- 提交阶段也是可中断的,如果在此期间有更高优先级的任务出现,React 会暂停提交过程,处理完高优先级任务后再继续提交。
总结
React Fiber 通过将渲染工作分解成小任务,并利用浏览器的空闲时间来执行这些任务,实现了可中断的更新过程。这种机制不仅提高了应用的响应性,还允许 React 更好地处理复杂的 UI 更新。优先级系统确保了重要的更新能够快速反映到界面上,而不重要的更新则可以在不影响用户体验的情况下延后处理。
# react 渲染流程
React 的渲染流程是一个高度优化且分阶段的过程,旨在确保高效地更新用户界面。整个渲染流程可以大致分为以下几个阶段:Scheduler(调度)、Reconciliation(协调)、Render(渲染)、和 Commit(提交)。下面将详细介绍每个阶段的作用和过程。
# 1. Scheduler(调度)
目的:确定何时执行更新任务,确保高优先级任务优先得到处理。
过程:
- 当应用状态发生变化或接收到外部事件时,React 会创建一个更新任务。
- React 使用调度器(Scheduler)来确定这些任务的优先级。调度器会根据任务的类型(如用户交互、定时器、网络请求等)分配不同的优先级。
- 调度器还会根据当前浏览器环境的空闲时间来安排任务的执行,确保用户界面的响应性。
# 2. Reconciliation(协调)
目的:检测并最小化虚拟 DOM 中的变化,生成最小的更新操作。
过程:
- Diffing 算法:当组件的状态或属性发生变化时,React 会生成一个新的虚拟 DOM 树。React 使用一种高效的 diffing 算法来比较新旧虚拟 DOM 树之间的差异,找出需要更新的部分。
- Fiber 架构:从 React 16 开始,React 引入了 Fiber 架构。Fiber 是一种内部的数据结构,用于表示组件树的节点。Fiber 架构使得 React 能够更细粒度地控制渲染过程,支持时间切片和优先级调度,从而提高性能和响应性。
# 3. Render(渲染)
目的:计算出需要更新的 UI。
过程:
- 组件生命周期方法:在这个阶段,React 会调用组件的生命周期方法,如
render
方法(对于类组件)或函数体本身(对于函数组件)。 - JSX 转换:
render
方法或函数组件返回的 JSX 会被转换成虚拟 DOM 节点。 - 时间切片:在渲染阶段,React 可以中断渲染任务,以便处理更高优先级的任务,如用户输入。这是通过将渲染任务分成多个小块并在每个块之间检查是否有更高优先级的任务来实现的。
# 4. Commit(提交)
目的:将更新应用到实际的 DOM。
过程:
- 更新 DOM:React 会根据上一阶段计算出的差异,更新实际的 DOM 节点。
- 生命周期方法:在这个阶段,React 会调用一些生命周期方法,如
componentDidMount
、componentDidUpdate
和componentWillUnmount
(对于类组件),以及useEffect
钩子中的回调函数(对于函数组件)。 - 不可中断:
commit
阶段是不可中断的,以确保 DOM 更新的一致性和完整性。
# 优化建议
- 使用
React.memo
和PureComponent
:避免不必要的组件重新渲染。 - 使用
shouldComponentUpdate
:手动控制组件是否需要更新。 - 懒加载组件:使用
React.lazy
和Suspense
来按需加载组件,提高初始加载性能。 - 避免在渲染阶段执行耗时操作:如复杂的计算或网络请求,可以使用
useEffect
在渲染后异步执行。 - 使用
useCallback
和useMemo
:优化性能,避免不必要的函数或对象重新创建。
# 总结
React 的渲染流程是一个多阶段的过程,每个阶段都有其特定的目的和任务。通过理解这些阶段的工作原理,开发者可以更好地优化应用的性能和用户体验。具体来说,React 的渲染流程可以概括为:
- Scheduler:确定任务的优先级和执行时机。
- Reconciliation:检测虚拟 DOM 的变化,生成最小的更新操作。
- Render:计算需要更新的 UI。
- Commit:将更新应用到实际的 DOM。
通过这些阶段的协同工作,React 能够高效地管理和更新用户界面,提供流畅的用户体验。
# react diff 流程
React 的 Diff 算法是 React 用来比较虚拟 DOM(Virtual DOM)树的两个不同版本,并找出最小的变更集以更新实际 DOM 的算法。这个过程也被称为协调(Reconciliation)。下面是 React Diff 算法的大致流程:
树结构差异计算:
- 当组件的状态或属性发生变化时,React 会生成新的虚拟 DOM 树。
- React 不会对整棵树进行逐节点对比,因为这样效率太低。它使用了启发式算法来提高效率。
同级元素比较:
- React 只会在同一层级上比较节点,不会跨层级比较。这意味着如果一个元素在旧树中存在而在新树中不存在,那么它的子节点会被完全删除;反之亦然。
- 如果两个元素的类型相同(例如都是
<div>
),则 React 会认为这两个元素代表的是相同的 UI 对象,然后继续递归地比较它们的属性和子节点。 - 如果两个元素的类型不同,则 React 会销毁旧元素并创建新元素,因为不同类型元素产生的 DOM 结构可能完全不同。
列表元素的比较:
- 对于列表中的元素,React 使用 key 来识别哪些元素改变了、添加了或删除了。
- 如果列表项没有指定 key,React 将默认使用索引作为 key,但这可能会导致不必要的重排,特别是在列表项被重新排序的情况下。
- 当列表项有稳定的 key 时,React 能够更准确地判断出哪些项是新增的、哪些项是移除的、哪些项是移动位置的,从而优化渲染过程。
组件实例的复用:
- 对于组件来说,如果组件的类型不变,React 会尝试复用现有的组件实例,只更新变化的 props 和 state。
- 如果组件类型变了,React 会卸载旧的组件实例,挂载新的组件实例。
DOM 更新:
- 最后,React 会根据上面计算出的差异,批量执行必要的 DOM 操作,比如插入、删除或修改节点,以使浏览器中的实际 DOM 与最新的虚拟 DOM 保持一致。
优化策略:
- React 还采用了一些优化策略,如 Fiber 架构,它允许 React 更细粒度地控制任务的调度,实现更好的用户体验,比如在用户交互过程中暂停一些不重要的更新。
通过这些步骤,React 的 Diff 算法能够高效地找到两棵虚拟 DOM 树之间的差异,并且只需要应用最少的更改到真实 DOM 上,从而提升应用性能。
# 举例说明 diff
当然可以。我们可以通过一个简单的例子来说明 React 的 Diff 算法是如何工作的。假设我们有一个简单的列表组件,它渲染了一个项目列表,并且这个列表中的项会根据某些操作(比如添加、删除或重新排序)而发生变化。
旧的虚拟 DOM 树
<ul>
<li key="1">Item 1</li>
<li key="2">Item 2</li>
<li key="3">Item 3</li>
</ul>
新的虚拟 DOM 树
<ul>
<li key="2">Item 2</li>
<li key="4">Item 4</li>
<li key="1">Item 1</li>
</ul>
Diff 过程
比较根节点:
- 两个树的根节点都是
<ul>
,类型相同,继续比较子节点。
- 两个树的根节点都是
比较第一个子节点:
- 旧的第一个子节点是
key="1"
。 - 新的第一个子节点是
key="2"
。 - 因为 key 不同,React 会寻找新树中是否有
key="1"
的节点。 - 发现新树中的第三个子节点是
key="1"
,这意味着原来的第一个子节点被移动到了第三个位置。
- 旧的第一个子节点是
比较第二个子节点:
- 旧的第二个子节点是
key="2"
。 - 新的第二个子节点也是
key="2"
。 - key 相同,继续递归比较这两个节点的内容。在这个例子中,内容没有变化,所以不需要做任何操作。
- 旧的第二个子节点是
处理新增节点:
- 新树中有一个新的节点
key="4"
,这在旧树中不存在,因此 React 会插入一个新的<li>
元素到 DOM 中。
- 新树中有一个新的节点
更新 DOM:
- 移动
key="1"
的元素到第三个位置。 - 插入
key="4"
的新元素作为第二个子节点。 - 删除
key="3"
的元素,因为这个 key 在新树中不存在。
- 移动
结果
最终,DOM 将被更新以匹配新的虚拟 DOM 树:
<ul>
<li>Item 2</li>
<li>Item 4</li>
<li>Item 1</li>
</ul>
总结
通过这个例子,我们可以看到 React 的 Diff 算法如何利用 key 来识别哪些节点发生了改变、哪些需要移动以及哪些需要插入或删除。这样做的好处是可以避免不必要的 DOM 操作,提高应用的性能。使用稳定的 key 可以帮助 React 更高效地执行这些操作。