本系列会实现一个简单的react
,包含最基础的首次渲染,更新,hook
,lane
模型等等,本文是本系列的第一篇。这对于我也是一个很大的挑战,不过这也是一个学习和进步的过程,希望能坚持下去,一起加油!期待多多点赞!😘😘
本文致力于实现一个最简单的优化策略执行过程,代码均已上传至github
,期待star!✨:
本文是系列文章,阅读的联系性非常重要!!
手写mini-react!超万字实现mount首次渲染流程🎉🎉
进击的hooks!实现react(反应)(反应)中的hooks架构和useState 🚀🚀
面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭
react(反应)(反应)能力值+1!useTransition是如何实现的?
面试官惊呼内行!万字解析React Suspense实现原理 🚀🚀
期待点赞!😁😁
食用前指南!本文涉及到react的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!
一. 性能优化策略
react中的性能优化的一般思路:将 「变化的部分」 与 「不变的部分」 分离。
什么是 「变化的部分」 ?
State
Props
Context
命中 「性能优化」 的组件可以不通过reconcile
生成wip.child
,而是直接复用上次更新生成的wip.child
。
总结起来有两点:
- 性能优化的思路是将 「变化的部分」 与 「不变的部分」 分离
- 命中性能优化的组件的子组件 (而不是他本身)不需要
render
源码内部有哪些性能优化策略?
存在两种性能优化策略:
bailout
策略:减少不必要的子组件render
eagerState
策略:不必要的更新,没必要开启后续调度流程
下面分别是用几个例子感受一下这两种策略:
- 例一
先来看一个小例子,这面这一段代码实现每点击一次对num
进行累加:
js
import { useState } from "react";
export default function App() {
const [num, update] = useState(0);
console.log("App render", num);
return (
<div>
<button onClick={() => update(num + 1)}> + 1</button>
<p>num is: {num}</p>
<ExpensiveSubtree />
</div>
);
}
function ExpensiveSubtree() {
console.log("Expensive render");
return <p>i am child</p>;
}
可以看到,每点击一次,无论是父组件<App />
还是子组件<ExpensiveSubtree />
都会重新渲染一遍:
但是其实我们只要认真分析一下代码就会发现,需要变动的部分就只有父组件涉及到num
的地方需要更新重新渲染,子组件<ExpensiveSubtree />
与上次更新相比没有发生任何变化,子组件根本就不需要重新渲染。
那么根据上面 react 的优化策略的原则改造一下我们的案例,将 「变化的部分」 与 「不变的部分」 分离,我们将<App />
组件中的更新num
的操作分离出来:
js
import { useState } from "react";
export default function App() {
console.log("App render");
return (
<div>
<Num />
<ExpensiveSubtree />
</div>
);
}
function Num() {
const [num, update] = useState(0);
return (
<div>
<button onClick={() => update(num + 1)}> + 1</button>
<p>num is: {num}</p>
</div>
);
}
function ExpensiveSubtree() {
console.log("Expensive render");
return <p>i am child</p>;
}
可以看到,当满足了react性能优化的策略之后,只会重新渲染与更新数据相关的组件。
除了首次加载触发的渲染逻辑,再次点击按钮,<App />
和<ExpensiveSubtree />
不会重新渲染。
为什么<App />
组件也不会重新渲染呢,这是因为它作为HostRoot
(根组件)节点的子组件,也命中了性能优化策略。
- 例二
再来看第二个小例子:
js
import { useState } from "react";
export default function App() {
console.log("App render");
const [num, update] = useState(0);
return (
<div title={num}>
<button onClick={() => update(num + 1)}> + 1</button>
<p>num is: {num}</p>
<ExpensiveSubtree />
</div>
);
}
function ExpensiveSubtree() {
console.log("Expensive render");
return <p>i am child</p>;
}
现在父组件里面也应用到了num
状态的变更,应该怎么改造,才能使ExpensiveSubtree
组件不会执行多余的渲染动作呢?
把<App />
组件中具有副作用的部分拆分出来,将子组件作为参数传入,类似于插槽的用法,使用children
进行渲染。
js
import { useState } from "react";
export default function App() {
console.log("App render");
return (
<Wrapper>
<ExpensiveSubtree />
</Wrapper>
);
}
function Wrapper({ children }) {
const [num, update] = useState(0);
return (
<div title={num}>
<button onClick={() => update(num + 1)}> + 1</button>
<p>num is: {num}</p>
{children}
</div>
);
}
function ExpensiveSubtree() {
console.log("Expensive render");
return <p>i am child</p>;
}
由于使用children
渲染的方式是使用组件的props
属性来获取的,而Wrapper
组件的props
属性是在<App />
组件的return
里面被设置的,所以当<App />
组件满足性能优化的策略时,<App />
组件return
的子级实际上是上次更新的结果,所以ExpensiveSubtree
组件也是复用上次的结果。Wrapper
组件中props
的children
属性也是不会变的。
可以看到,依然可以实现性能优化的目的:
- 例三
js
import { useState } from "react";
import ReactDOM from "react-dom/client";
export default function App() {
const [num, update] = useState(0);
console.log("App render ", num);
return (
<div
onClick={() => {
update(1);
}}
>
<Cpn />
</div>
);
}
function Cpn() {
console.log("cpn render");
return <div>cpn</div>;
}
在这个例子中,首次渲染的时候两个组件都会渲染,在使用update
方法触发组件更新时,第一次会触发数据的更新和组件的重新渲染。但是第一次再使用update
方法触发更新时组件不会再更新了,这是因为由于每次数据的变动都是相同的值,并没有使num
变化,数据没有更新。所以组件不会再次出发渲染流程。
但是这里有一个问题,在上面的上个例子中,父组件中的更新行为都已经被拆分出父组件,所以父组件命中性能优化策略,同时子组件也不会重新渲染。但是我们的例三中父组件明明存在数据更新,为什么子组件还能避免重复更新呢?这是因为react会进行两种情况的判断一种是没有状态的情况,对应例一和例二。还有一种是具有状态变化,但是更新前后没有变化,所以也会命中性能优化策略,子组件也不会重新渲染。
那么两种情况有什么不同之处呢?
第一种情况因为父组件都没有状态变化,所以不需要render
的过程。
第二种情况因为具备状态变化,所以需要render
的过程,需要计算一下更新前后的值有没有变化,到底需不需要重新渲染。所以这也就是为什么例三中的App
组件为什么最后会打印一次。而子组件则不会被打印。
这个过程就是前面的bailout
策略。后续之所以怎么点击都不会再次更新是因为发现每次更新的值都是1,没有必要为本次更新开启调度流程,也不会进入render
阶段,这就是另一种eagerState
策略,也就是针对不必要的更新,没必要开启后续调度流程。
二. lanes 工作流程
上图是简洁的 react 执行流程。
由于bailout
策略中需要涉及到判断state
的逻辑,所以需要判断当前fiber
是否存在未执行的update
。
那么如何才能得知当前是否还有未执行的update
呢?众所周知,每一次发生的更新都会创建一个update
对象保存到fiber
节点的更新队列中。
js
// useState dispatch
function dispatchSetState(
fiber,
updateQueue,
action
) {
// 定义lane优先级
const lane = requestUpdateLane();
// 创建update对象
const update = createUpdate(action, lane);
// 添加到update队列
enqueueUpdate(updateQueue, update);
// 开始调度更新
scheduleUpdateOnFiber(fiber, lane);
}
由于在整个更新流程中 react 不是每一次都是同步更新,而是根据不同的触发行为划分不同的执行时机,这也就是在前面几篇文章中实现的lane
调度的逻辑。
在之前lane
调度的基础上,为fiber
节点新增lanes
的逻辑:
fiber.lanes
保存一个fiberNode
中 「所有未执行更新对应的lane」。所以利用 lanes
的逻辑,我们只需要判断fiber
节点下的lanes
是否还有lane
(未执行完的优先级)。
每次更新时的enqueueUpdate
中处理lanes
的逻辑:
diff
// useState dispatch
function dispatchSetState(
fiber,
updateQueue,
action
) {
// 定义lane优先级
const lane = requestUpdateLane();
// 创建update对象
const update = createUpdate(action, lane);
// 添加到update队列
// 为enqueueUpdate传入本次更新优先级lane和fiber节点
++ enqueueUpdate(updateQueue, update, fiber, lane);
// 开始调度更新
scheduleUpdateOnFiber(fiber, lane);
}
enqueueUpdate
的作用主要是将本次更新的update
对象添加到fiber
节点的shared.pending
属性中,并组成一条环形链表。
在处理完更新对象后,将本次更新的lane
合并到fiber
的lanes
属性中。值得注意的是,由于我们经常需要通过双缓存树current
中对应的节点进行重置数据,所以在合并优先级的时候,需要将current
树中的节点也进行优先级的合并。
js
export const enqueueUpdate = (
updateQueue,
update,
fiber,
lane
) => {
const pending = updateQueue.shared.pending;
// ...
updateQueue.shared.pending = update;
// 合并lane优先级
fiber.lanes = mergeLanes(fiber.lanes, lane);
// 合并current树中fiber节点的优先级
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
};
// 合并
export function mergeLanes(laneA, laneB) {
return laneA | laneB;
}
在创建处理完update
对象后,最终会在beginWork
函数中,也就是render
阶段处理fiber
节点的起点根据不同类型进行消费。通过update
来计算state
。
但是由于更新优先级的存在,某些优先级不够的更新可能会被跳过,所以对应的lane
也就不会被消费。
在执行update
时,如果遇到被跳过的更新,执行传入的回调函数:
js
export const processUpdateQueue = (
baseState,
pendingUpdate,
renderLane,
onSkipUpdate
) => {
const result = {
memoizedState,
baseState,
baseQueue
};
if (pendingUpdate !== null) {
// ...
do {
const updateLane = pending.lane;
if (!isSubsetOfLanes(renderLane, updateLane)) {
// 优先级不够 被跳过
// 克隆当前的update对象
const clone = createUpdate(pending.action, pending.lane);
// 将本次的更新对象传入回调函数
onSkipUpdate?.(clone);
// ...
} else {
// ...
}
pending = pending.next;
} while (pending !== first);
// ...
}
return result;
};
在使用useState
对数据进行更新时,dispatch
触发update
更新任务,调用processUpdateQueue
执行更新任务。
js
export const beginWork = (wip, renderLane) => {
// 清空lanes
wip.lanes = NoLanes;
// ...
}
由于在执行到beginWork
时fiber
节点的lanes
已经被清空。在通过dispatch
触发更新时,如果有因为优先级不足而没有被执行的update
对象,它的lane
会被重新加入到fiber
节点的lanes
属性中。
js
function updateState() {
// 找到当前useState对应的hook数据
const hook = updateWorkInProgressHook();
// ...
if (baseQueue !== null) {
const prevState = hook.memoizedState;
const {
memoizedState,
baseQueue: newBaseQueue,
baseState: newBaseState
} = processUpdateQueue(baseState, baseQueue, renderLane, (update) => {
const skippedLane = update.lane;
const fiber = currentlyRenderingFiber;
// NoLanes
// 重新加入被跳过的update更新对象的lane
fiber.lanes = mergeLanes(fiber.lanes, skippedLane);
});
// ...
}
return [hook.memoizedState, queue.dispatch];
}
除了在fiber
节点中增加lanes
属性记录所有触发的优先级之外。再增加一个属性childLanes
,用于保存当前fiber
节点下所有子节点的lane
,保存的时机可以选择在render
流程的回溯时,也就是在completeWork
中。根据element
创建fiber
节点时会执行beginWork
函数不断创建子fiber
节点,当处理到最后一个子节点时,查找兄弟节点,如果没有兄弟节点,执行completeWork
函数,回到父级。然后不断重复这个过程,直到最后回到根节点。
所以在收集fiber
节点所有子节点的lane
时,可以选择在completeWork
回溯时进行收集,因为他会经过每个子节点最后聚拢在父节点。
在此之前我们已经利用completeWork
函数收集每个子fiber
节点的副作用标记。
js
export const completeWork = (wip) => {
// 递归回溯
const newProps = wip.pendingProps;
const current = wip.alternate;
switch (wip.tag) {
case HostComponent:
// ...
bubbleProperties(wip);
return null;
case HostText:
// ...
bubbleProperties(wip);
return null;
case HostRoot:
case FunctionComponent:
case Fragment:
bubbleProperties(wip);
return null;
default:
if (__DEV__) {
console.warn('未处理的completeWork情况', wip);
}
break;
}
};
completeWork
根据不同的类型执行不同的处理,最后都会调用bubbleProperties
收集副作用和lane
到父fiber
中。
js
function bubbleProperties(wip) {
let subtreeFlags = NoFlags;
let child = wip.child;
let newChildLanes = NoLanes;
while (child !== null) {
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
// 收集childLanes
// 合并子级的lanes和子级的所有childLanes
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
);
child.return = wip;
child = child.sibling;
}
// 收集副作用标记
wip.subtreeFlags |= subtreeFlags;
wip.childLanes = newChildLanes;
}
在每次触发更新时,例如在dispatch
时会从发生更新的函数组件的fiber
节点开始向上查找到根节点,然后从根节点开始生成完成的workInProgress
树。由于在触发更新时也会生成一个对应的lane
,所以在向上查找根节点的过程也需要保存childLanes
。
scheduleUpdateOnFiber
为一次更新render
阶段的入口函数。
js
export function scheduleUpdateOnFiber(fiber, lane) {
// fiberRootNode 由触发更新的节点开始查找根节点
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
// ...
}
在向上合并的同时,也需要更新current
树的childLanes
属性。
js
export function markUpdateLaneFromFiberToRoot(fiber, lane) {
let node = fiber;
let parent = node.return;
// 不断向上查找父节点
while (parent !== null) {
// 合并
parent.childLanes = mergeLanes(parent.childLanes, lane);
// 合并current树childLanes
const alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
}
node = parent;
parent = node.return;
}
if (node.tag === HostRoot) {
return node.stateNode;
}
return null;
}
三. bailout 策略
命中 「性能优化」 (bailout
策略)的组件可以不通过reconcile
生成wip.child
,而是直接复用上次更新生成的wip.child
。
bailout
策略存在于beginWork
中
bailout
四要素:
props
不变
比较props
变化是通过 「全等比较」 ,使用React.memo
后会变为 「浅比较」
state
不变
两种情况可能造成state
不变:
- 不存在
update
- 存在
update
,但计算得出的state
没变化
context
不变type
不变
workLoop
函数控制当前正在处理哪一个fiber
节点,workInProgress
相当于一个fiber
指针(全局变量),指向当前正在处理的fiber
节点。
每次beginWork
处理完一个fiber
节点,因为beginWork
函数的作用就是根据element
对象生成子节点的fiber
节点,所以在beginWork
函数执行完毕后,会将生成好的子级fiber
节点return
。然后会将workInProgress
指针指向beginWork
函数返回的fiber
节点,如果没有子级说明已经到达最底部,返回null
。如果不为null
,workLoop
会继续调用performUnitOfWork
,就相当于开启子级的beginWork
。
js
function workLoop() {
// workInProgress不为null,继续调用
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(fiber) {
// 返回的next就是生成的子级fiber
const next = beginWork(fiber, wipRootRenderLane);
fiber.memoizedProps = fiber.pendingProps;
// 如果为null,开始completeWork流程
// 不为null,继续生成子fiber
if (next === null) {
completeUnitOfWork(fiber);
} else {
workInProgress = next;
}
}
从beginWork
函数开始真正开始创建fiber
节点,所以我们的bailout
策略在beginWork
执行之前进行判断。
bailout
策略的判定
js
// 是否需要执行更新
let didReceiveUpdate = false;
export const beginWork = (wip, renderLane) => {
// bailout策略
didReceiveUpdate = false;
const current = wip.alternate;
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = wip.pendingProps;
// 判断 props type 属性是否一致
if (oldProps !== newProps || current.type !== wip.type) {
didReceiveUpdate = true;
} else {
// 是否有更新?
const hasScheduledStateOrContext = checkScheduledUpdateOrContext(
current,
renderLane
);
if (!hasScheduledStateOrContext) {
// 命中bailout策略
// state和context不变
didReceiveUpdate = false;
return bailouOnAlreadyFinishedWork(wip, renderLane);
}
}
}
wip.lanes = NoLanes;
// 比较,返回子fiberNode
switch (wip.tag) {
case HostRoot:
return updateHostRoot(wip, renderLane);
case HostComponent:
return updateHostComponent(wip);
case FunctionComponent:
return updateFunctionComponent(wip, wip.type, renderLane);
case HostText:
// ...
}
return null;
};
获取current
树的对应属性,首先判断 props
和 type
属性是否一致。查看属性是否发生了变化。如果两个属性不一致,说明需要执行更新didReceiveUpdate = true
)。
js
if (oldProps !== newProps || current.type !== wip.type){}
如果一致,需要验证当前的fiber
节点是否有需要更新的update
对象:
本次更新要执行的优先级,当前的fiber
中也存在这个优先级代表的update
。
hasScheduledStateOrContext
为true
说明有需要的更新任务,state
和context
可能会被更新,所以不会命中bailout
策略。如果没有当前优先级的更新任务,则说明state
和context
没有更新。
js
function checkScheduledUpdateOrContext(
current,
renderLane
) {
const updateLanes = current.lanes
if (includeSomeLanes(updateLanes, renderLane)) {
return true;
}
return false;
}
export function includeSomeLanes(set, subset) {
return (set & subset) !== NoLanes;
}
如果不存在更新任务,命中bailout
策略:
js
function bailouOnAlreadyFinishedWork(wip, renderLane) {
// 本次更新不在子节点的lanes中
if (!includeSomeLanes(wip.childLanes, renderLane)) {
if (__DEV__) {
console.warn('bailout整棵子树', wip);
}
return null;
}
// 处理当前未bailout
if (__DEV__) {
console.warn('bailout一个fiber', wip);
}
// 克隆子节点及子节点的兄弟节点
cloneChildFibers(wip);
return wip.child;
}
此时需要判断如果此fiber
下的所有子树是否还存在与本次更新同一个lane
的更新任务,如果没有,直接返回null
,代表执行优化程度高的执行逻辑。workLoop
不会继续向下处理fiber
节点。
bailouOnAlreadyFinishedWork
存在两种优化路径:优化程度高的话,整个子树不需要更新操作,跳过子树的beginwork
流程。
优化程度低的话,只需要复用这个命中策略的fiber
节点的子节点,所以克隆子节点并返回
如果子树还存在与本次相同lane
的更新任务,执行优化程度低的逻辑将子节点克隆复用:
js
export function cloneChildFibers(wip) {
// child sibling
if (wip.child === null) {
return;
}
let currentChild = wip.child;
//
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
wip.child = newChild;
newChild.return = wip;
// 复用兄弟节点
while (currentChild.sibling !== null) {
currentChild = currentChild.sibling;
newChild = newChild.sibling = createWorkInProgress(
newChild,
newChild.pendingProps
);
newChild.return = wip;
}
}
当然,是不是被跳过的更新没有机会命中bailout
策略了呢?在HostRoot
和FunctionComponent
类型各自的处理函数中,也存在相应的处理逻辑:
HostRoot
类型代表处理根节点,就是我们初始的<App />
函数,所以也会存在状态的变更:
js
function updateHostRoot(wip, renderLane) {
// ...
const prevChildren = wip.memoizedState;
const nextChildren = wip.memoizedState;
if (prevChildren === nextChildren) {
return bailouOnAlreadyFinishedWork(wip, renderLane);
}
// 处理子节点
reconcileChildren(wip, nextChildren);
return wip.child;
}
FunctionComponent
:
当类型为函数组件类型时,在函数内部可能存在useState
等触发更新的hook。也就是函数内部存在update
,但计算得出的state
没变化这种情况。所以也需要判断是否会命中bailout
策略。
js
function updateFunctionComponent(
wip,
Component,
renderLane
) {
// ...
// render
// 执行函数组件
const nextChildren = renderWithHooks(wip, Component, renderLane);
// ...
return wip.child;
}
函数组件在renderWithHooks
中被执行,比如存在useState
这个hook,在useState
更新阶段会执行updateState
函数更新内部保存的值:
js
function updateState() {
// 找到当前useState对应的hook数据
const hook = updateWorkInProgressHook();
// 计算新state的逻辑
if (baseQueue !== null) {
const prevState = hook.memoizedState;
// 计算本次更新最新的state
const {
memoizedState,
baseQueue: newBaseQueue,
baseState: newBaseState
} = processUpdateQueue();
// 和上次更新相比,state是否发生了变化
// 如果不一致,没有命中,标记didReceiveUpdate为需要更新
if (!Object.is(prevState, memoizedState)) {
markWipReceivedUpdate();
}
// ...
}
return [hook.memoizedState, queue.dispatch];
}
// 标记需要执行更新
export function markWipReceivedUpdate() {
didReceiveUpdate = true;
}
在renderWithHooks
函数组件执行完之后,说明state
值已经更新,是否命中bailout
策略已经有了定论,根据didReceiveUpdate
的值对子节点执行不同的处理逻辑。
diff
function updateFunctionComponent(
wip,
Component,
renderLane
) {
// ...
// render
// 执行函数组件
const nextChildren = renderWithHooks(wip, Component, renderLane);
const current = wip.alternate;
++ if (current !== null && !didReceiveUpdate) {
++ // 命中bailout,复用子节点
++ bailoutHook(wip, renderLane);
++ return bailouOnAlreadyFinishedWork(wip, renderLane);
}
// 未命中bailout策略,生成子节点fiber
reconcileChildren(wip, nextChildren);
return wip.child;
}
不过在函数组件中当命中bailout
策略后,需要重置各种属性,手动移除lane
:
js
export function bailoutHook(wip, renderLane) {
const current = wip.alternate;
wip.updateQueue = current.updateQueue;
wip.flags &= ~PassiveEffect;
// 移除lane
current.lanes = removeLanes(current.lanes, renderLane);
}
// 移除lane
export function removeLanes(set, subet) {
return set & ~subet;
}
四. eagerState 策略
状态更新前后没有变化,那么没有必要触发更新,为此需要做:
- 计算更新后的状态
- 与更新前的状态做比较
通常情况下, 「根据update计算state」 发生在beginWork
,而我们需要在 「触发更新时」 计算状态:
只有满足 「当前fiberNode没有其他更新」 才尝试进入eagerState
策略。
useState
在触发更新时使用dispatch
派发更新,内部调用dispatchSetState
。
在没有加入eagerState
策略时,调用dispatch
更新状态首先创建一个更新对象update
并加入fiber
节点的更新队列,随后通过scheduleUpdateOnFiber
开始调度更新。
所以eagerState
策略首先要在dispatch
发起后先判断状态有没有变化,如果没有变化且lanes
中不存在优先级(没有待执行的更新任务),就不会发起调度任务。
js
function dispatchSetState(
fiber,
updateQueue,
action
) {
const lane = requestUpdateLane();
// 创建update对象
const update = createUpdate(action, lane);
// eager策略
const current = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(current === null || current.lanes === NoLanes)
) {
// 1. 更新前的状态 2.计算状态的方法
// 上次更新的state
const currentState = updateQueue.lastRenderedState;
// 本次计算后新的state
const eagarState = basicStateReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagarState;
// 两次state是否一致
if (Object.is(currentState, eagarState)) {
// 加入更新队列,不携带lane
enqueueUpdate(updateQueue, update, fiber, NoLane);
// 命中eagerState
if (__DEV__) {
console.warn('命中eagerState', fiber);
}
return;
}
}
// 没命中
// 加入更新队列,携带lane
enqueueUpdate(updateQueue, update, fiber, lane);
// 调度更新
scheduleUpdateOnFiber(fiber, lane);
}
basicStateReducer
针对用户传入dispatch
的更新函数或者值对state
进行更新。
js
export function basicStateReducer(
state,
action
) {
// 函数 -> 执行
if (action instanceof Function) {
return action(state);
} else {
// 值 -> 直接返回
return action;
}
}
其中lastRenderedState
属性是在上次执行更新流程的hook函数时被保存。
js
function updateState() {
// 找到当前useState对应的hook数据
const hook = updateWorkInProgressHook();
// ...
if (baseQueue !== null) {
const prevState = hook.memoizedState;
const {
memoizedState,
baseQueue: newBaseQueue,
baseState: newBaseState
} = processUpdateQueue(baseState, baseQueue, renderLane, (update) => {
// ...
});
// ...
hook.memoizedState = memoizedState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueue;
// 保存更新后的state状态
queue.lastRenderedState = memoizedState;
}
return [hook.memoizedState, queue.dispatch];
}
写在最后 ⛳
未来可能会更新实现mini-react
和antd
源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳