Appearance
实现篇
第五章 Diff 算法
概览
本文探讨 React 中 Diff 算法的实现和其性能优化策略。Diff 算法用于比较 DOM 更新中新旧元素的差异,并据 此进行高效的 DOM 操作。在 React 中,每个 DOM 节点可以与四种类型的对象关联:
- current Fiber:当前在页面中的 DOM 节点对应的 Fiber 节点。
- workInProgress Fiber:即将在本次更新中渲染到页面的 DOM 节点对应的 Fiber 节点。
- DOM 节点本身。
- JSX 对象:描述 DOM 节点的信息,可以是 ClassComponent 的**
render**方法的返回结果或是 FunctionComponent 的调用结果。
Diff 算法的目的是通过比较 1 和 4 来生成 2。
Diff 算法的性能瓶颈和 React 的解决策略
React 避免了 O(n^3)的完全树比较算法,采用了一些预设限制以优化性能:
- 只对同级元素进行 Diff。
- 不同类型的元素将生成不同的树。
- 通过**
key** prop 暗示哪些子元素可以复用。
例子
不使用 key:
JavaScript
// Before
<div>
<p>ka</p><h3>song</h3>
</div>
// After
<div>
<h3>song</h3>
<p>ka</p>
</div>// Before
<div>
<p>ka</p><h3>song</h3>
</div>
// After
<div>
<h3>song</h3>
<p>ka</p>
</div>使用 key:
JavaScript
// Before
<div>
<p key="ka">ka</p><h3 key="song">song</h3>
</div>
// After
<div>
<h3 key="song">song</h3>
<p key="ka">ka</p>
</div>// Before
<div>
<p key="ka">ka</p><h3 key="song">song</h3>
</div>
// After
<div>
<h3 key="song">song</h3>
<p key="ka">ka</p>
</div>在使用**key**的情况下,React 知道哪些 DOM 节点可以复用。
Diff 算法的实现
Diff 算法的入口是**reconcileChildFibers**函数,它根据新子节点(newChild)的类型来调用不同的处理函 数。例如:
JavaScript
function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// Handle with reconcileSingleElement
// ...other cases
}
}
// ...other types
return deleteRemainingChildren(returnFiber, currentFirstChild);
}function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// Handle with reconcileSingleElement
// ...other cases
}
}
// ...other types
return deleteRemainingChildren(returnFiber, currentFirstChild);
}这里可以将 Diff 过程分为两类:
- 当**
newChild**为**object**、**number**、**string**时,同级只有一 个节点。 - 当**
newChild****为** **Array**时,同级有多个节点。
各种情况都有对应的处理函数,如**reconcileSingleElement**和**reconcileChildrenArray**等。
综上,React 的 Diff 算法通过各种优化策略和实现细节,达到了高效 DOM 操作的目的。这些优化策略和实现细 节共同构成了 React 高性能特性的一个重要部分。
单节点 Diff
在 React 的虚拟 DOM 更新机制中,一个关键环节是判断哪些 DOM 节点可以被复用。这一步是通 过**reconcileSingleElement**函数来完成的。复用机制不仅影响性能,还涉及到页面渲染的准确性。本文将用 简洁明了的方式,结合代码例子,解释这一过程。
reconcileSingleElement 函数
这个函数的核心逻辑是遍历现有的“fiber”(代表页面上实际 DOM 节点的内部结构),然后与新生成的 ReactElement 进行比较。比较的标准是两个:key**和**type。
代码片段:
JavaScript
while (child !== null) {
if (child.key === key) {
switch (child.tag) {
default: {
if (child.elementType === element.type) {
return existing; // 可复用
}
break;
}
}
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}while (child !== null) {
if (child.key === key) {
switch (child.tag) {
default: {
if (child.elementType === element.type) {
return existing; // 可复用
}
break;
}
}
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}DOM 复用规则
- 相同 key,相同 type: 可复用。
- 相同 key,不同 type: 不可复用,且该 fiber 及其兄弟 fiber 都将被删除。
- 不同 key: 不可复用,仅删除当前 fiber。
多节点 Diff
理解 React 的 Diff 算法是前端开发中的一个重要内容,**这个算法负责比较新旧两个虚拟 DOM,找出差异并最 小化更新实际 DOM。**在这篇文章里,我将概括地解释 React Diff 算法的核心逻辑和主要概念,同时保留代码例 子以便理解。
基本场景
Diff 算法主要处理以下三种情况:
- 节点更新: 当一个节点的属性或类型发生变化时。
- 节点新增或减少: 当 DOM 结构中增加或删除了节点。
- 节点位置变化: 当同级别的多个节点的相对位置发生变化。
例如,节点更新的代码例子如下:
JavaScript
// 之前
<ul>
<li key="0" className="before">0</li>
<li key="1">1</li>
</ul>
// 之后 —— 节点属性变化
<ul>
<li key="0" className="after">0</li>
<li key="1">1</li>
</ul>// 之前
<ul>
<li key="0" className="before">0</li>
<li key="1">1</li>
</ul>
// 之后 —— 节点属性变化
<ul>
<li key="0" className="after">0</li>
<li key="1">1</li>
</ul>Diff 算法思路
React 的 Diff 算法遵循两轮遍历的原则:
- 第一轮遍历: 主要处理节点更新。这里优先级被赋予更新操作,因为在实际应用中更新操作更为频繁。
- 第二轮遍历: 处理节点新增、减少或位置变化。
第一轮遍历
在第一轮遍历中,每一个新的子节点(newChildren[i])都与当前的旧Fiber(oldFiber)进行比较,以判 断是否可以复用 DOM 节点。
JavaScript
let i = 0;
while (i < newChildren.length && oldFiber !== null) {
// 比较newChildren[i]与oldFiber
}let i = 0;
while (i < newChildren.length && oldFiber !== null) {
// 比较newChildren[i]与oldFiber
}两种情况可能导致节点不可复用:
- **
key****值不同。** - 同一个**
key**值但是类型不同。
第一种情况下,整个第一轮遍历会结束。第二种情况下,旧的 Fiber 节点会被标记为DELETION,然后继续遍 历。
第二轮遍历
第二轮遍历会处理三种场景:
- 两者都遍历完: 直接结束 Diff。
- **
newChildren**没遍历完,**oldFiber****遍历完:** 对剩下的newChildren进 行Placement标记。 - **
newChildren**遍历完,**oldFiber****没遍历完:** 对剩下的oldFiber进 行Deletion标记。
最复杂的场景是当两者都没有遍历完。这时,节点可能会在更新中改变位置。为了找到这些移动的节点,React 使 用key作为唯一标识。
处理移动的节点
在处理移动的节点时,React 使用key和lastPlacedIndex来判断节点是否移动。
JavaScript
let lastPlacedIndex = 0;
let oldIndex = getOldIndexByKey(key);
if (oldIndex < lastPlacedIndex) {
// 进行移动操作
} else {
lastPlacedIndex = oldIndex;
}let lastPlacedIndex = 0;
let oldIndex = getOldIndexByKey(key);
if (oldIndex < lastPlacedIndex) {
// 进行移动操作
} else {
lastPlacedIndex = oldIndex;
}通过这种方式,React 的 Diff 算法能以相对较高的效率处理 DOM 的更新,从而提供更好的用户体验。希望这篇 文章能帮助你更好地理解 React Diff 算法的内部工作原理。
第六章 状态更新
流程概览
理解 React 的状态更新机制对于深入了解这个流行库的内部工作原理至关重要。下面是一个简化的总结,突出显 示该过程中的关键节点。
触发状态更新
不同的方法可以用来触发状态更新,例如:
ReactDOM.renderthis.setStatethis.forceUpdateuseStateuseReducer
这些方法虽然用于不同的场景,但最终都会导向同一套状态更新机制。
创建 Update 对象
每次状态更新都会创建一个**“Update”对象来保存更新状态相关的信息。**在接下来的render阶 段,**beginWork函数会用这个 Update 对象来计算新的state**。
JavaScript
const update = {
payload: null,
next: null
};const update = {
payload: null,
next: null
};从 Fiber 到 Root
触发状态更新的 Fiber 节点现在包含一个 Update 对象。你需要找到这个 Fiber 节点对应的 rootFiber。这是通 过调用 **markUpdateLaneFromFiberToRoot****函数**完成的。
JavaScript
const root = markUpdateLaneFromFiberToRoot(fiber);const root = markUpdateLaneFromFiberToRoot(fiber);调度更新
有了 rootFiber,接下来就是告知调度器(Scheduler)根据更新的优先级,以同步或异步的方式进行调度。
JavaScript
if (newCallbackPriority === SyncLanePriority) {
newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}if (newCallbackPriority === SyncLanePriority) {
newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}Render 阶段
依据调度结果,执行**performSyncWorkOnRoot或performConcurrentWorkOnRoot****,开始 **render阶段。
Commit 阶段
render阶段完成后,进入commit阶段,通过调用commitRoot来应用状态更新。
JavaScript
commitRoot(root);commitRoot(root);总结
以上就是 React 状态更新流程的一个大致概览。这个流程是 React concurrent mode 的核心机制之一,也是 实现状态管理、异步更新等高级功能的基础。
心智模型
在讨论 React 的更新机制之前,建立一个心智模型是非常有用的。通过这个模型,我们将更容易地理解源码 和实际操作背后的逻辑。
同步更新的 React
心智模型:代码版本控制
在没有代码版本控制的情况下,我们逐渐添加新的代码和功能。这一切都看似有序,直到遇到一个紧急的线上 bug(我们将其标记为红色节点)。
JavaScript
// 流程1:同步更新
ReactDOM.render(<App />, rootElement);// 流程1:同步更新
ReactDOM.render(<App />, rootElement);实际操作
在这种模式下,所有通过ReactDOM.render创建的 React 应用都会按照**先进先出(FIFO)**的方式更新状态。 也就是说,没有优先级概念,紧急的(红色节点)更新需要等待之前所有的更新执行完毕。
并发更新的 React
心智模型:代码版本控制 + 分支
当使用代码版本控制,我们可以暂存当前的更改,切换到主分支去修复紧急的 bug,并将其快速部署到线上。然 后,通过 git rebase,我们可以让开发分支在修复了 bug 的新版本上继续开发。
JavaScript
// 流程2:创建并发root
const root = ReactDOM.createRoot(rootElement);
root.render(<App />);// 流程2:创建并发root
const root = ReactDOM.createRoot(rootElement);
root.render(<App />);实际操作
通过ReactDOM.createBlockingRoot或ReactDOM.createRoot创建的应用会采用并发的方式进行状态更 新。高优先级的更新(红色节点)会中断正在进行的低优先级更新(标记为蓝色节点),并先完成整个渲染和 提交的流程。
JavaScript
// 流程3:高优先级更新会打断低优先级更新
root.render(<HighPriorityUpdate />);// 流程3:高优先级更新会打断低优先级更新
root.render(<HighPriorityUpdate />);一旦高优先级更新完成,低优先级的更新会在这个新的基础上重新开始。
总结
通过类比代码版本控制系统,我们可以更容易地理解 React 的更新机制。同步更新类似于没有分支的版本控 制,而并发更新则像是有分支和回滚功能的版本控制,能更灵活地处理紧急情况。这两种模式分别通 过**ReactDOM.render**和**ReactDOM.createRoot**来实现,它们在优先级处理和中断机制上有着本 质的不同。这不仅有助于我们理解 React 的源码,还能指导我们如何更有效地使用 React。
Update
当我们在使用 React 进行开发时,你可能会遇到各种类型的组件和更新机制。为了更好地理解这一切如何运作, 了解 React 内部的Update和Fiber结构是非常有用的。下面是一个精简但详尽的概览。
触发更新的组件类型
- **
ReactDOM.render**HostRoot - **
this.setState**ClassComponent - **
this.forceUpdate**ClassComponent - **
useState**FunctionComponent - **
useReducer**FunctionComponent
这里有三种组件(HostRoot、ClassComponent、FunctionComponent)可以触发更新。
Update 的基本结构
Update的基本结构在 ClassComponent 和 HostRoot 是相同的,示例如下:
JavaScript
const update = {
eventTime,
lane,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};const update = {
eventTime,
lane,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};字段解释:
eventTime: 任务时间lane: 优先级tag: 更新类型(UpdateState, ReplaceState 等)payload: 要更新的数据callback: 更新后的回调next: 指向下一个 Update,形成链表
Update 与 Fiber 的关系
Fiber**节点上的多个**Update**会组成链表并存储在**fiber.updateQueue**中。 这样,一个**Fiber**节点可能会有多个与之相关的**Update。
Update Queue
Update Queue 的结构与 Update 类型有关。对于 ClassComponent 和 HostRoot,其结构大致如下:
JavaScript
const queue = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};const queue = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};实际操作示例
假设有一个 fiber 节点,上面有两个未处理的 Update,称为 u1和u2。
JavaScript
fiber.updateQueue.firstBaseUpdate === u1;
fiber.updateQueue.lastBaseUpdate === u2;fiber.updateQueue.firstBaseUpdate === u1;
fiber.updateQueue.lastBaseUpdate === u2;执行两次状态更新,产生u3和u4。
Plaintext
fiber.updateQueue.shared.pending === u4;
u4.next === u3;
u3.next === u4;fiber.updateQueue.shared.pending === u4;
u4.next === u3;
u3.next === u4;在 render 阶段,这些 Updates 将被处理:
JavaScript
fiber.updateQueue.baseUpdate: u1 --> u2 --> u3 --> u4fiber.updateQueue.baseUpdate: u1 --> u2 --> u3 --> u4最后,计算出新的 state 并完成页面渲染。
通过这样的机制,React 能够以非常高效和可控的方式进行状态管理和 UI 渲染。希望这篇文章能帮助你更深入地 理解 React 的内部机制。
深入理解优先级
什么是优先级
在 React 的运行机制中,状态更新不是立即执行的,而是有一套优先级机制来调度这些更新。这样做的目的是根 据人机交互的研究来更符合用户的使用习惯和预期。
例如:
- 生命周期方法:同步执行
- 受控的用户输入:比如输入框,同步执行
- 交互事件:如动画,高优先级
- 其他:如数据请求,低优先级
如何调度优先级
React 使用 Scheduler 来进行任务调度。Scheduler 里的 runWithPriority 方法接受一个优先级 和一个回调函数作为参数,然后根据优先级来排列这些任务。
JavaScript
runWithPriority(UserBlockingPriority, () => {
performUnitOfWork(workInProgress);
});runWithPriority(UserBlockingPriority, () => {
performUnitOfWork(workInProgress);
});优先级如何决定更新的顺序
在一个示例中,假设有两个 Update,一个由 "关闭黑夜模式" 触发,我们称其为 u1;另一个由输入字母 "I" 触发,称为 u2。
初始状态:
JavaScript
fiber.updateQueue = {
baseState: {
blackTheme: true,
text: 'H'
},
shared: {
pending: u1
},
effects: null
};fiber.updateQueue = {
baseState: {
blackTheme: true,
text: 'H'
},
shared: {
pending: u1
},
effects: null
};假设 u1 的执行被高优先级的 u2 打断,此时,队列变为:
JavaScript
fiber.updateQueue.shared.pending = u2 ----> u1fiber.updateQueue.shared.pending = u2 ----> u1这里 u2.next === u1**,表示 u2 有更高的优先级。**然后它会先执行,跳过 u1。
如何保证状态正确
如何保证 Update 不丢失
当 render 阶段被中断后重新开始时,会基于 current updateQueue 克隆出 workInProgressupdateQueue。由于 current.updateQueue.lastBaseUpdate 已经保存了上一次的 Update,所以 Update 不会 丢失。
如何保证状态依赖的连续性
当一个 Update 被跳过时,所有比它优先级低的 Update 也会被保存,以保证状态更新的连续性。
JavaScript
// 第一次 render
baseState: ''
baseUpdate: null
// 使用的 Updates: [A1, C1]
memoizedState: 'AC'
// 第二次 render
baseState: 'A'
baseUpdate: B2 --> C1 --> D2
// 使用的 Updates: [B2, C1, D2]
memoizedState: 'ABCD'// 第一次 render
baseState: ''
baseUpdate: null
// 使用的 Updates: [A1, C1]
memoizedState: 'AC'
// 第二次 render
baseState: 'A'
baseUpdate: B2 --> C1 --> D2
// 使用的 Updates: [B2, C1, D2]
memoizedState: 'ABCD'这样做是为了保证状态更新的连续性和前后依赖关系。
总结,React 的优先级机制与状态更新是一个相当精妙的设计,它不仅能更好地匹配用户交互,还有一套完备的机 制来保证状态更新的准确性和连续性。
ReactDOM.render
当我们谈到 React 应用程序的运行机制**ReactDOM.render()**是其中的关键步骤。这一函数涉及许多底层操 作,包括但不限于 **fiber 的创建、更新的调度以及渲染。**下面,我们将逐一剖析这些步骤。
创建 fiber
调用ReactDOM.render()首先会进入 **legacyRenderSubtreeIntoContainer**方法,并在这里创 建 fiberRootNode 和 rootFiber。
JavaScript
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;在legacyCreateRootFromDOMContainer内部,**createFiberRoot**函数负责实现 fiberRootNode 和 rootFiber 的创建。
JavaScript
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean
): FiberRoot {
const root: FiberRoot = new FiberRootNode(containerInfo, tag, hydrate);
const uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
initializeUpdateQueue(uninitializedFiber);
return root;
}export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean
): FiberRoot {
const root: FiberRoot = new FiberRootNode(containerInfo, tag, hydrate);
const uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
initializeUpdateQueue(uninitializedFiber);
return root;
}创建 Update
接下来,**updateContainer****方法**用于创建一个 Update 对象。
JavaScript
const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = {element};
enqueueUpdate(current, update);
scheduleUpdateOnFiber(current, lane, eventTime);const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = {element};
enqueueUpdate(current, update);
scheduleUpdateOnFiber(current, lane, eventTime);其中,update.payload = {element};的element就是ReactDOM.render()的第一个参数。
流程概览
总体来说,整个流程如下:
- 创建fiberRootNode、rootFiber、updateQueue(
legacyCreateRootFromDOMContainer) - 创建 Update 对象(
updateContainer) - 从 fiber 到 root(
markUpdateLaneFromFiberToRoot) - 调度更新(
ensureRootIsScheduled) - render 阶段(
performSyncWorkOnRoot或performConcurrentWorkOnRoot) - commit 阶段(
commitRoot)
React 的不同模式
React 有三种模式:
- legacy:当前的默认模式。
- blocking:实验性质,开启部分 concurrent 模式特性。
- concurrent:面向未来的模式,包括任务中断和任务优先级。
这些模式有不同的入口函数:
- legacy:
ReactDOM.render(<App />, rootNode) - blocking:
ReactDOM.createBlockingRoot(rootNode).render(<App />) - concurrent:
ReactDOM.createRoot(rootNode).render(<App />)
虽然每种模式的入口函数不同,它们都影响**fiber.mode**,但对上述的整体流程没有影响。
通过这样的剖析,我们更容易理解**ReactDOM.render()**的内部逻辑和 React 的不同工作模式。希望这篇文章 能帮助你深入理解 React 的底层机制。
this.setState
React 中的状态更新和渲染是一门复杂的艺术。这篇文章概括地介绍了如何从调用setState或forceUpdate到 实际 DOM 更新的全过程。我们将主要探讨两个方面:HostRoot 的更新和 ClassComponent 的更新。
HostRoot 更新流程
当你首次执行ReactDOM.render(),React 会创建fiberRootNode和rootFiber。这两者分别代表应用的根节 点和组件树的根节点。
JavaScript
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
const uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
const uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;在这之后,React 会创建一个 Update 对象,这个对象包含了需要更新的元素和其他信息。
JavaScript
const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = {element};const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = {element};ClassComponent 更新流程
在类组件中,this.setState()会触发一个与 HostRoot 类似的更新过程。核心代码如下:
JavaScript
this.updater.enqueueSetState(this, partialState, callback, 'setState');this.updater.enqueueSetState(this, partialState, callback, 'setState');这里,enqueueSetState函数负责创建 Update 对象,并把它放入更新队列(updateQueue)。
JavaScript
const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = payload;const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = payload;Force Update
如果你使用this.forceUpdate(),一个特殊标记ForceUpdate会被加入到 Update 对象中。
JavaScript
update.tag = ForceUpdate;update.tag = ForceUpdate;这确保了组件会重新渲染,忽略shouldComponentUpdate或PureComponent优化。
总结
无论是 Root 组件还是 ClassComponent,React 都采用了类似的更新流程。核心思想是创建一个 Update 对象, 然后加入到更新队列中,最后调度更新。这个流程为 React 提供了高度的灵活性和可扩展性,同时也支持了各种 优化策略。
在下一篇文章中,我们将探讨用于 Hooks 的 Update 机制,它又是另一种有趣的数据结构。
第七章 Hooks
Hooks 理念
引言
React 框架中的设计元素反映了多层次的抽象,从 ClassComponent 到现代的 Hooks,每一种元素都代表着对框架 内部运行规则的不同级别的理解。本文将深入探讨为何 Hooks 能更贴近 React 的核心设计,以及其与 Concurrent Mode 的关系。
React 的基础组成:原子与电子
React 的 LOGO 中使用了代表原子的图案。在这个框架中,UI 可以被拆分成许多独立的单元,称为 Component。 这些 Components 就像世界由原子组成一样。然而,原子不是最基础的组成单元;科学家们还在原子中发现了电 子。
在 React 的世界里,ClassComponents 是相当于“原子”,而 Hooks 则更像是“电子”。
React 运行流程
React 遵循 schedule-render-commit 的运行流程,这个流程决定了 React 如何更新和维护其 UI。ClassComponents 提供了一个更高级别的抽象,以便开发者能更容易地理解和使用这个框架。
JavaScript
class MyComponent extends React.Component {
componentDidUpdate() {
console.log('Component did update');
}
}class MyComponent extends React.Component {
componentDidUpdate() {
console.log('Component did update');
}
}Hooks 更接近 React 的运行规律
相对于 ClassComponents,Hooks 提供了更接近 React 内部运行规律的抽象。Hooks 使开发者能够更灵活地管理 state、context 和生命周期。
JavaScript
useEffect(() => {
console.log('something updated');
}, [props.something]);useEffect(() => {
console.log('something updated');
}, [props.something]);但值得注意的是,这两者并不完全等价。比如,在替代 componentWillReceiveProps 的过程中,你可能会选择 使用 useEffect,但实际上这两者的执行时机并不相同。
Concurrent Mode 和 Hooks
Concurrent Mode 是 React 的未来发展方向。Hooks 能够最大限度地发挥 Concurrent Mode 的潜力。这种构建方 式更贴近 React 内部的工作方式,因此,Hooks 可以被视为 React 世界中的“电子”。
总结
React Hooks 提供了一个更接近 React 内部运行规则的抽象层,相对于 ClassComponents,它能更好地利用 Concurrent Mode 的优势。这种接近底层的抽象也解释了为什么 Hooks 被视为 React 世界中的“电子”而非“原子 ”。这些都表明,理解 Hooks 的设计原则能帮助我们更高效地使用 React 框架。
极简 Hooks 实现
React 的 Hooks API 为函数组件带来了状态管理和副作用处理的能力。这里我们用不到 100 行代码实现一个简化 版的useState Hook,以更好地理解其工作原理。
工作原理
考虑以下例子:
JavaScript
function App() {
const [num, updateNum] = useState(0);
return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;
}function App() {
const [num, updateNum] = useState(0);
return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;
}这个过程可以分为两个主要部分:
- 通过某种方式产生更新,使组件重新渲染。
- 在组件重新渲染时,
useState返回的num会是更新后的值。
更新是什么
在简化版中,一个"更新"可以用以下结构表示:
JavaScript
const update = {
action,
next: null
};const update = {
action,
next: null
};其中,action是一个函数,用于计算新的状态。例如,在上面的App组件中,点击<p>标签时产生 的action为num => num + 1。
更新的数据结构
这些更新会形成一个环状单向链表。
JavaScript
function dispatchAction(queue, action) {
const update = {action, next: null};
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
schedule();
}function dispatchAction(queue, action) {
const update = {action, next: null};
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
schedule();
}如何保存状态
与类组件不同,函数组件没有实例来存储数据。因此,我们使用一个简化版的fiber对象来存储。
JavaScript
const fiber = {
memoizedState: null,
stateNode: App
};const fiber = {
memoizedState: null,
stateNode: App
};Hook 的数据结构
JavaScript
hook = {
queue: {
pending: null
},
memoizedState: initialState,
next: null
}hook = {
queue: {
pending: null
},
memoizedState: initialState,
next: null
}模拟 React 的更新调度
我们用一个isMount变量来区分组件是首次渲染(mount)还是更新(update)。
JavaScript
isMount = true;
function schedule() {
workInProgressHook = fiber.memoizedState;
fiber.stateNode();
isMount = false;
}isMount = true;
function schedule() {
workInProgressHook = fiber.memoizedState;
fiber.stateNode();
isMount = false;
}实现 useState
现在我们来实现useState。
JavaScript
function useState(initialState) {
let hook;
if (isMount) {
hook = { queue: { pending: null }, memoizedState: initialState, next: null };
if (!fiber.memoizedState) {
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
} else {
hook = workInProgressHook;
workInProgressHook = workInProgressHook.next;
}
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending.next)
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}function useState(initialState) {
let hook;
if (isMount) {
hook = { queue: { pending: null }, memoizedState: initialState, next: null };
if (!fiber.memoizedState) {
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
} else {
hook = workInProgressHook;
workInProgressHook = workInProgressHook.next;
}
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending.next)
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}与真实 React 的区别
- 真实的 React Hooks 使用不同的调度器(dispatcher)来区分 mount 和 update。
- 真实的 React Hooks 具有跳过不必要更新的优化。
- 真实的 React Hooks 有 batchedUpdates,允许在一个周期内合并多个更新。
- 真实的 React Hooks 的更新有优先级概念。
通过这个简化版本的useState,我们能更清晰地理解其背后的运行逻辑和数据结构。这只是入门级别的示例,真 实的 React 库中有更多高级特性和优化。
Hooks 数据结构
在 React 中,Hooks 提供了一种高效的方法来在函数组件中处理状态和副作用。为了理解 Hooks 如何内部工作, 本文会深入解析 React Hooks 的数据结构和其核心概念。
Dispatcher
在 React 的源码中,Dispatcher 是一个对象,它管理着不同生命周期(Mount 和 Update)中的 hook 函数。有 两个主要的 Dispatcher:HooksDispatcherOnMount 和 HooksDispatcherOnUpdate。
JavaScript
// mount时的Dispatcher
const HooksDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect,
// ...其他
};
// update时的Dispatcher
const HooksDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect,
// ...其他
};// mount时的Dispatcher
const HooksDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect,
// ...其他
};
// update时的Dispatcher
const HooksDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect,
// ...其他
};React 使用全局变量**ReactCurrentDispatcher来确定应使用哪个 Dispatcher。这是通过检查当前的 Fiber 节点(代表 React 组件)的current** 和 memoizedState 属性来实现的。
JavaScript
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;错误处理
当你错误地嵌套地使用 Hooks,如在**useEffect中使 用useState,ReactCurrentDispatcher.current会指向一个ContextOnlyDispatcher**,这将导 致抛出异常。
JavaScript
export const ContextOnlyDispatcher = {
useState: throwInvalidHookError,
// ...其他
};export const ContextOnlyDispatcher = {
useState: throwInvalidHookError,
// ...其他
};Hook 的数据结构
一个基本的 Hook 数据结构大致如下:
JavaScript
const hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};const hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};memoizedState 是一个特别重要的字段,它保存了 Hook 的当前状态或数据。
不同类型的 memoizedState
- useState: 对于**
useState(initialState),memoizedState保存state**的值。 - useReducer: 对于**
useReducer(reducer, {}),也是保存state**的值。 - useEffect: **
memoizedState**保存包含回调函数、依赖项等的链表数据结构。 - useRef: 对于**
useRef(1)**,保存的是{current: 1}。 - useMemo: 对于**
useMemo(callback, [depA])**,保存的是[callback(), depA]。 - useCallback: 对于**
useCallback(callback, [depA])**,保存的是[callback, depA]。
一些 Hooks(如**useContext)可能没有memoizedState**。
通过这些数据结构和 Dispatcher 的管理,React Hooks 提供了一种强大而灵活的方式来在函数组件中管理状态和 副作用。这不仅简化了组件代码,还提高了整体性能。希望这篇文章能帮助你更深入地理解 React Hooks 的内部 机制。
useState 与 useReducer
React Hooks 的两个重要成员是 useState 和 useReducer,它们都用于管理组件的内部状态。下面我们将了 解这两个 Hooks 在声明阶段和调用阶段的工作原理。
声明阶段
当一个 Function Component 开始渲染,useReducer 和 useState 被调用。这两个 Hook 的基本源码大致如 下:
JavaScript
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function useReducer(reducer, initialArg, init) {
var dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArg, init);
}function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function useReducer(reducer, initialArg, init) {
var dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArg, init);
}resolveDispatcher 解析出一个 dispatcher 对象,这个对象会根据当前场景(mount 或 update)来决定调用 哪个实现。
mount 时
mount 阶段的代码实现如下:
JavaScript
function mountState<S>(initialState: S): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
const queue = (hook.queue = { /* ... */ });
return [hook.memoizedState, dispatch];
}
function mountReducer<S, I, A>(reducer: (S, A) => S, initialArg: I, init?: I => S): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
const queue = (hook.queue = { /* ... */ });
return [hook.memoizedState, dispatch];
}function mountState<S>(initialState: S): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
const queue = (hook.queue = { /* ... */ });
return [hook.memoizedState, dispatch];
}
function mountReducer<S, I, A>(reducer: (S, A) => S, initialArg: I, init?: I => S): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
const queue = (hook.queue = { /* ... */ });
return [hook.memoizedState, dispatch];
}两者唯一的区别是 queue 对象的 lastRenderedReducer 字段。
调用阶段
当用户点击按钮时,dispatch 或 updateNum 被调用,此时会触发 dispatchAction。
JavaScript
codefunction dispatchAction(fiber, queue, action) {// 创建 update 对象var update = { /* ... */ }; // 将 update 加入 queue.pending// ...scheduleUpdateOnFiber(fiber, lane, eventTime); codefunction dispatchAction(fiber, queue, action) {// 创建 update 对象var update = { /* ... */ }; // 将 update 加入 queue.pending// ...scheduleUpdateOnFiber(fiber, lane, eventTime);其他要点
useReducer的reducer参数是可变的,即每次调用useReducer时都会更新lastRenderedReducer。
总结
useState本质上是预置了 reducer 的useReducer。- 在 mount 阶段,两者的主要区别在于用于处理更新的 reducer。
- 调用阶段主要是关于如何处理更新队列和调度新的渲染。
通过了解这些细节,你能更深入地理解 React 的状态管理机制,这有助于你更有效地使用 useState 和 useReducer。
useEffect
React 的useEffect钩子允许我们在函数组件内部执行副作用操作,如数据获取、订阅或者手动改变 DOM 等。该 钩子有两个重要阶段:销毁阶段和回调执行阶段。这两个阶段被精心设计以确保组件之间不会互相干扰,尤其是在 多个组件可能共享相同ref的情况下。
代码流程与优先级
useEffect的工作开始于flushPassiveEffects方法,该方法负责设置优先级并调 用flushPassiveEffectsImpl来执行实际操作。在这里,主要有三个任务需要完成:
- 调用上一次
render时的销毁函数(如果有)。 - 调用本次
render时的回调函数。 - 如果存在同步任务,不需要等待下次事件循环的宏任务,提前执行它。
JavaScript
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = unmountEffects[i];
const fiber = unmountEffects[i + 1];
const destroy = effect.destroy;
effect.destroy = undefined;
if (typeof destroy === 'function') {
try {
destroy();
} catch (error) {
captureCommitPhaseError(fiber, error);
}
}
}const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = unmountEffects[i];
const fiber = unmountEffects[i + 1];
const destroy = effect.destroy;
effect.destroy = undefined;
if (typeof destroy === 'function') {
try {
destroy();
} catch (error) {
captureCommitPhaseError(fiber, error);
}
}
}销毁阶段
在销毁阶段(阶段一),React 会遍历一个名为pendingPassiveHookEffectsUnmount的数组,该数组保存了所有 需要执行销毁的useEffect。数组的索引i保存需要销毁的effect,i+1保存该 effect 对应的fiber对象。
该数组在 layout 阶段的commitLayoutEffectOnFiber方法中得到填充。它用于保证在执行任何组件 的useEffect回调函数之前,所有的组件销毁函数都已被执行。这一设计是为了解决多个组件可能共用同一 个ref的问题。
回调执行阶段
与销毁阶段类似,回调执行阶段(阶段二)也是通过遍历一个数组来实现的,该数组名 为pendingPassiveHookEffectsMount。
JavaScript
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = unmountEffects[i];
const fiber = unmountEffects[i + 1];
const destroy = effect.destroy;
effect.destroy = undefined;
if (typeof destroy === 'function') {
try {
destroy();
} catch (error) {
captureCommitPhaseError(fiber, error);
}
}
}const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = unmountEffects[i];
const fiber = unmountEffects[i + 1];
const destroy = effect.destroy;
effect.destroy = undefined;
if (typeof destroy === 'function') {
try {
destroy();
} catch (error) {
captureCommitPhaseError(fiber, error);
}
}
}从同步到异步
值得注意的是,在 React 16 版本中,这些任务是同步执行的。但从 v16.13.1 开始,以及在 React 17 中,这些 都变成了异步执行,以优化大型应用程序的性能。
通过这样的设计,React 成功地实现了useEffect在复杂应用场景下的高效、可靠和可预测性。
useRef
在 React 中,ref 是一个非常重要的概念,通常用于保存对 DOM 节点或组件实例的引用。本文将详细介 绍useRef的实现以及ref的工作流程。
useRef 的基本实现
React Hooks 引入了useRef,允许你在函数组件中使用ref。其基础实现十分简单,其主要目的是创建并返回 一个包含current属性的对象。这个对象被保存在memoizedState中。
JavaScript
function mountRef<T>(initialValue: T): {|current: T|} {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
function updateRef<T>(initialValue: T): {|current: T|} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}function mountRef<T>(initialValue: T): {|current: T|} {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
function updateRef<T>(initialValue: T): {|current: T|} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}这与React.createRef的实现相似,后者也仅返回一个包含current的对象。
JavaScript
export function createRef(): RefObject {
const refObject = {
current: null,
};
return refObject;
}export function createRef(): RefObject {
const refObject = {
current: null,
};
return refObject;
}ref 的工作流程
在 React 中,你可以将ref赋值给HostComponent(如<div>, <span>等)、ClassComponent和ForwardRef。
JavaScript
// HostComponent
<div ref={domRef}></div>
// ClassComponent / ForwardRef
<App ref={cpnRef} />// HostComponent
<div ref={domRef}></div>
// ClassComponent / ForwardRef
<App ref={cpnRef} />render 阶段
在render阶段,React 会识别哪些组件需要操作ref,并为它们的fiber添加Ref标记(effectTag)。
JavaScript
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
workInProgress.effectTag |= Ref;
}
}function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
workInProgress.effectTag |= Ref;
}
}commit 阶段
在commit阶段,React 会遍历所有带有Ref标记的fiber对象,并进行相应的操作。
移除旧的 ref: 如果
ref发生改变,首先移除之前的ref。JavaScriptfunction commitDetachRef(current: Fiber) { const currentRef = current.ref; if (currentRef !== null) { if (typeof currentRef === 'function') { currentRef(null); } else { currentRef.current = null; } } }function commitDetachRef(current: Fiber) { const currentRef = current.ref; if (currentRef !== null) { if (typeof currentRef === 'function') { currentRef(null); } else { currentRef.current = null; } } }赋值新的 ref: 最后,在
commitAttachRef函数中,React 会为新的ref进行赋值。JavaScriptfunction commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { const instance = finishedWork.stateNode; let instanceToUse; switch (finishedWork.tag) { case HostComponent: instanceToUse = getPublicInstance(instance); break; default: instanceToUse = instance; } if (typeof ref === 'function') { ref(instanceToUse); } else { ref.current = instanceToUse; } } }function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { const instance = finishedWork.stateNode; let instanceToUse; switch (finishedWork.tag) { case HostComponent: instanceToUse = getPublicInstance(instance); break; default: instanceToUse = instance; } if (typeof ref === 'function') { ref(instanceToUse); } else { ref.current = instanceToUse; } } }
总结
useRef和ref在 React 中是十分重要的,他们允许你在组件中保存可变的值而不重新渲染组件。通 过effectTag机制,React 有效地标记和处理了那些需要操作ref的组件,确保了ref的正确和高效的更新。
希望这篇文章和代码例子能帮助你更好地理解 React 中的useRef和ref的工作机制。
useMemo 与 useCallback
React Hooks 是 React 库的一部分,它们提供了一种让函数组件具有类组件(class components)能力(例如, 状态管理和生命周期方法)的方式。useMemo和useCallback都是优化性能的钩子,但它们有细微的区别。这篇 文章将深入浅出地讲解这两个 Hooks 的实现。
mount 阶段
mountMemo
在 mount 阶段,mountMemo函数首先通过mountWorkInProgressHook函数创建一个新的 hook 对象。然后,它 计算nextCreate函数的执行结果,并将这个结果和依赖项(如果有)存储在hook.memoizedState中。
JavaScript
function mountMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}function mountMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}mountCallback
与mountMemo类似,mountCallback也通过mountWorkInProgressHook创建一个新的hook。但是,它直接保存 回调函数本身,而不是它的执行结果。
JavaScript
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}update 阶段
updateMemo
在 update 阶段,updateMemo检查依赖项是否改变。如果没有改变,它直接返回之前计算的值;否则,它重新计 算nextCreate函数的执行结果。
JavaScript
function updateMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null && areHookInputsEqual(nextDeps, prevState[1])) {
return prevState[0];
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}function updateMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null && areHookInputsEqual(nextDeps, prevState[1])) {
return prevState[0];
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}updateCallback
这个函数的逻辑与updateMemo非常相似,但是它保存的是回调函数本身,而不是它的执行结果。
JavaScript
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null && areHookInputsEqual(nextDeps, prevState[1])) {
return prevState[0];
}
hook.memoizedState = [callback, nextDeps];
return callback;
}function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null && areHookInputsEqual(nextDeps, prevState[1])) {
return prevState[0];
}
hook.memoizedState = [callback, nextDeps];
return callback;
}总结
useMemo和useCallback在 mount 阶段的区别是:useMemo保存的是回调函数nextCreate的执行结果, 而useCallback保存的是回调函数本身。- 在 update 阶段,两者的行为几乎一致,只是
useMemo会重新计算执行结果,而useCallback则会直接保存 新的回调函数。
通过了解这两个 Hooks 的内部实现,我们更容易理解它们的用途和行为,从而在开发过程中做出更合适的选择。
第八章 Concurrent Mode
概览
引言
Concurrent Mode 是 React 的新模式,专门为实现应用的异步更新和优先级管理而设计。此模式是 React Fiber 架构的自然延伸,旨在使应用更加响应。
底层架构:Fiber
Fiber 是 React 的计算单位,每个组件都对应一个 Fiber。该架构使 React 能够进行"异步可中断的更新",即在 需要的情况下可以暂停、继续或取消组件的渲染。
JavaScript
// Fiber 对应于每个组件
const fiber = {
stateNode: Component, // 组件实例
// 其他属性
};// Fiber 对应于每个组件
const fiber = {
stateNode: Component, // 组件实例
// 其他属性
};驱动器:Scheduler
Scheduler 负责管理时间切片,它根据环境的性能动态地分配可运行时间给每个 Fiber。这样,即使在低性能设备 上,用户界面也能保持响应。
JavaScript
// 以时间切片方式运行
Scheduler.unstable_runWithPriority(priority, () => {
// 执行任务
});// 以时间切片方式运行
Scheduler.unstable_runWithPriority(priority, () => {
// 执行任务
});运行策略:Lane 模型
Lane 模型用于管理不同优先级的更新。例如,用户交互产生的更新可能具有更高的优先级,而后台数据拉取的更 新则具有较低的优先级。
JavaScript
// 通过 lane 控制优先级
const lane = getLane(priority);// 通过 lane 控制优先级
const lane = getLane(priority);上层实现:新功能
batchedUpdates
多个更新可被合并为一个,减少不必要的渲染。
JavaScript
onClick() {
this.setState({stateA: 1});
this.setState({stateB: false});
this.setState({stateA: 2});
}onClick() {
this.setState({stateA: 1});
this.setState({stateB: false});
this.setState({stateA: 2});
}Suspense
允许在数据请求过程中显示一个临时状态,提高用户体验。
JavaScript
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense><Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense>useDeferredValue
可以返回一个延迟响应的值,提供更高的界面响应性。
JavaScript
javascriptCopy codeconst deferredValue = useDeferredValue(value, { timeoutMs: 2000 });javascriptCopy codeconst deferredValue = useDeferredValue(value, { timeoutMs: 2000 });总结
Concurrent Mode 是 React 的未来发展方向,它基于底层的 Fiber 架构和 Scheduler 驱动器,通过 Lane 模型 实现多优先级更新管理。上层实现如 batchedUpdates、Suspense 和 useDeferredValue 使 React 应用更加高效 和用户友好。预计未来会有更多基于这一模式的新功能和库出现。
lane模型
React 使用一个独特的优先级模型,称为"lane 模型",以解决应用中各种更新场景的优先级问题。该模型特别适 用于 React 的 Concurrent Mode,让我们更好地理解和管理复杂的状态和界面更新。
赛道(Lane)的概念
想象一个赛车场,不同的赛车在不同的赛道上运行。内圈赛道更短,外圈赛道更长。在这个模型中,每个"赛 道"(Lane)代表一个特定优先级的更新。
例如:
JavaScript
export const SyncLane: Lane = 0b0000000000000000000000000000001;export const SyncLane: Lane = 0b0000000000000000000000000000001;这里,SyncLane代表一个同步优先级的更新。
批(Lanes)的概念
"批"是一组具有相似优先级的更新,用多个"赛道"(Lane)来表示。例如:
JavaScript
const InputDiscreteLanes: Lanes = 0b0000000000000000000000000011000;const InputDiscreteLanes: Lanes = 0b0000000000000000000000000011000;这里,InputDiscreteLanes占用了两个位,表示用户交互触发的高优先级更新。
优先级相关计算
由于每个赛道都对应二进制中的一个位,优先级的计算可以通过位运算轻松完成。
例如,要检查两个赛道(或批)是否有交集:
JavaScript
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
return (a & b) !== NoLanes;
}export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
return (a & b) !== NoLanes;
}总结
React 的 lane 模型为开发者提供了一种强大的方式来处理不同类型和优先级的更新。这种方法允许更高级的计算 和更灵活的更新调度,尤其在使用 Concurrent Mode 时。
通过这种方式,React 确保了用户界面的流畅性,同时也使开发者能更容易地管理复杂的状态和更新。
异步可中断更新
在现代前端开发中,页面更新的复杂性逐渐增加。React 作为前端框架的佼佼者,引入了一系列先进的概念来处理 这种复杂性。其中一个最引人注目的特性就是异步可中断的更新机制,在 React 的 Concurrent Mode 中尤为重 要。
异步更新与同步更新
在传统的同步更新中,一旦开始,整个更新周期—从渲染到 DOM 更新—都是一气呵成的。与之相反,在异步更新模 式下,React 能够“中断”这个过程,使得高优先级的更新能插队执行。
JavaScript
// 同步更新例子
ReactDOM.render(<App />, root);
// 异步更新例子
ReactDOM.createRoot(root).render(<App />);// 同步更新例子
ReactDOM.render(<App />, root);
// 异步更新例子
ReactDOM.createRoot(root).render(<App />);优先级机制:Lane 模型
React 引入了 Lane 模型来处理不同优先级的更新。这个模型为不同类型的更新分配了不同的“赛道”(Lane)。
JavaScript
export const SyncLane: Lane = 0b0000000000000000000000000000001;
export const InputDiscreteLanes: Lanes = 0b0000000000000000000000000011000;export const SyncLane: Lane = 0b0000000000000000000000000000001;
export const InputDiscreteLanes: Lanes = 0b0000000000000000000000000011000;可中断与可恢复
异步更新的最大优点是它是“可中断”的。这意味着一个低优先级的更新(例如由于数据请求引发的更新)可以被一 个高优先级的更新(如用户交互)中断。
JavaScript
const highPriorityUpdate = useTransition();
highPriorityUpdate(() => {
// 高优先级更新
});const highPriorityUpdate = useTransition();
highPriorityUpdate(() => {
// 高优先级更新
});代码执行:位运算
异步更新机制下,React 使用位运算来执行优先级的计算,这大大提高了效率。
JavaScript
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
return (a & b) !== NoLanes;
}export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
return (a & b) !== NoLanes;
}实践应用:Concurrent Mode
在 Concurrent Mode 下,React 更是大量利用了异步可中断的更新特性。例如,通 过useTransition和useDeferredValue等 Hook,React 能更智能地管理复杂的状态和 UI 更新。
JavaScript
const [startTransition, isPending] = useTransition();
startTransition(() => {
// 可以被中断的更新
});const [startTransition, isPending] = useTransition();
startTransition(() => {
// 可以被中断的更新
});结论
React 的异步可中断更新是其在高性能、高响应的现代 Web 应用开发中的一项关键优势。通过引入像 Lane 模型 这样的高级优先级机制,以及通过 Concurrent Mode 的高级特性,React 能够提供更流畅、更灵活的用户体验。
高优任务打断机制
在交互密集型的 Web 应用中,如何优雅地处理多个并发任务是一个具有挑战性的问题。React 解决了这个问题, 主要依靠其高优(高优先级)任务打断机制。这个机制特别在 React 的 Concurrent Mode 下发挥作用,它允许高 优先级的任务可以打断正在执行的低优先级任务。
什么是高优任务?
高优任务通常是由用户交互触发的,比如点击、滚动等。这些任务需要快速响应,以提供良好的用户体验。
JavaScript
// 用户点击按钮触发高优任务
<button onClick={() => setCount(count + 1)}>Click Me</button>// 用户点击按钮触发高优任务
<button onClick={() => setCount(count + 1)}>Click Me</button>