Appearance
useState
在说明 useState 之前,我们得了解一下 Hooks 的作用是什么,useState 的作用是什么?
Hooks 的作用
Hooks 的出现使得 React 的 FunctionalComponent 有了对标 ClassComponent 的可能,那么 ClassComponent 具有的私有变量、生命周期管理的能力 Hooks 是如何实现的呐。
带这这个问题,我们就可以知道 Hooks 在聚合一个功能特性的时候,怎么去维护 FunctionalComponent 在初始化、更新、销毁三个状态的情况下使得函数也拥有了私有属性的能力且能在不同的生命周期实现不同的功能。
useState 的作用是什么
js
const [state, setState] = useState(initialState);返回一个 state,以及更新 state 的函数。
在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。
在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。
从官网的说明中我们可以看到其区别于函数中的变量的地方在于 在函数组件初次执行的时候使用的是(initialState) 值,但是在重新渲染的时候使用的却是更新后的值。
那么 useState(1) 在函数更新执行的时候 怎么知道 state 的值是 2... 而不是我们初始化的值 1,useState 如何保存函数执行的时候 state 的状态值的。
其实这个跟 classComponent 一样,class 对象为什么可以在缓存当前状态的值 就是因为 Class 对象在初次创建的时候会生成一个实例对象,其私有属性都缓存在这个实例对象上,那么 class.render 等方法执行的时候就可以通过方法实例对象的属性去获取当前状态下的值了。
那么对于 FunctionalComponent 本身其是一个函数,其不会产生状态,但是其返回的值为 FiberNode 对象就可以作为一个实例对象去缓存当前状态下的值。这就是函数式组件为什么可以存在私有变量的原因。
栗子
js
const App = (props) => {
// 添加一个 hook
const [visible, setVisible] = React.useState(true);
// 添加一个 hook
const [num, setNum] = React.useState(1);
const handleClickBtn = () => {
// setVisible hook上添加一个 update
setVisible(!visible);
};
React.useEffect(() => {
// setNum hook上添加一个 update
setNum(num + 1);
// setNum hook上添加一个 update
setNum(num + 1);
}, []);
return (
<div>
<button onClick={handleClickBtn}>切换 - {visible && "是"}</button>
</div>
);
};在父组件遇到 App 子组件的时候 会创建一个 App 的 FiberNode 对象,然后判断 FiberNode 的 tag 类型的时候发现其为 IndeterminateComponent 类型,从而进入到 mountIndeterminateComponent的流程,其核心代码为 renderWithHooks 。下面我们先了解一下对于这个栗子中Hooks的内容是如何进行保存的。
执行
const [visible, setVisible] = React.useState(true)这是一个 hook,那么就创建一个 hook 对象保存到 FiberNode(App).memoizedState 上
执行
const [num, setNum] = React.useState(1)又是一个 hook,那么再创建一个 hook 对象,发现 FiberNode(App).memoizedState 已经有值,那么就保存到 FiberNode(App).memoizedState.next 上
这样就形成了如上图所示的 hook 的 单向链表结构,
初次渲染
在初次渲染的时候,其渲染值 visible就是 hook.memoizedState的值true , num 就是第二个 hook 的 hook.memoizedState的值1。
dispatchSetState(触发修改)
上文都是说的如何在初次渲染或者更新渲染的时候 如何创建 hook 单向链表、复用 hook 单项链表和获取 state
下面我们主要看其第二个值 触发修改
需要注意的是,dispatchSetState 不是我们想象的怎么去计算出新的 state,其只是根据你的 dispatch 去创建一个 Update 对象,然后在组件更新渲染的时候,根据 update queue 去计算出真正的值
其主要逻辑如下:
生成当前的优先级
const lane = requestUpdateLane(fiber)生成 Update
js
const update: Update<S, A> = {
// 获取当前的优先级
lane,
// 更新回调函数
action,
// 是否直接计算过
hasEagerState: false,
// 直接计算的值
eagerState: null,
// 指向下一个 update
next: (null: any),
};- 根据不同的情况不同处理
- 2.1. 如果触发的时候正好是执行当前组件的渲染流程。
方案: 直接将 update 加入到 hook.queue 中去 执行时机: 在下一次渲染阶段且 update.lane 在 renderLane 中,那么就会在 useState 的时候去计算出 最新的值(hook.memoizedState)
- 2.2. 不是当前组件的渲染流程中
这里面有一个优化过程:如果当前正好没有渲染通道 , 这时候就可以先计算一下新的值,然后判断新旧值是否相同,在相同的情况不触发更新
js
function App() {
const [num, setNum] = React.useState(1);
useEffect(() => {
// setNum hook上添加一个 update
setNum((num) => num);
setNum((num) => num + 2);
}, []);
return <div>{num}</div>;
}如上述例子的 setNum((num) => num), 就会触发 dispathchSetState,但是这个时候发现 App 组件没有其他的 lane ,那么就可以直接计算出新的值,然后判断新旧值是否相同,在相同的情况不触发更新。
但是对于 setNum((num) => num + 2) 这种情况,也会触发 dispathchSetState,但是这时候会发现,AppFiberNode.lane 不是 NoLane(上一步添加了默认 lane),那么这个 update 就不会触发计算过程了
渲染 state 的计算时机:
在 setState 的时候,计算结果需要更新的时候会将 update.hasEagerState , 那么在更新渲染useState计算最新 state 的时候,遇到 update.hasEagerState 为 true 的时候,就会直接使用 update.eagerState 的值,而不是执行 update.action 去计算。
js
// 如果当前update 是渲染阶段添加的,其会预先计算出状态值,那么就直接使用结果
// 否则,就使用 reducer 计算出状态值
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}但是这边除了 return 的情况还调用了 enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
我们具体看一下
js
export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>
): void {
// This function is used to queue an update that doesn't need a rerender. The
// only reason we queue it is in case there's a subsequent higher priority
// update that causes it to be rebased.
const lane = NoLane;
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
}跟正常流程很像,也会将 update 加入到 concurrentQueues 队列中,只是不触发更新,这是为什么?
- 2.3. 正常情况
- 将 update 入队,并找到 HostRoot 节点
- 计算出一个优先级时间戳
- 调度更新
最后如果我们一个事件中进行了多次的 dispatch,那么这个过程就是将这些 dispath 按照执行的顺序,收集到 hook.pending 中去。
最后的结果就是:
js
React.useEffect(() => {
// setNum hook上添加一个 update
setNum((num) => num + 1);
// setNum hook上添加一个 update
setNum((num) => num + 2);
// setNum hook上添加一个 update
setNum((num) => num + 3);
// setNum hook上添加一个 update
setNum((num) => num + 4);
}, []);
注意:
- pending 本身不是我们想象的第一个 dispatch(num1) 而是最后一个 dispatch(num4)
- 第一个 dispatch : pending.next
js
/**
* 满满的Redux风格
* Hooks中 useState useReducer 等触发变量修改的方法
* @param {*} fiber action的FiberNode对象
* @param {*} queue hooks的queue对象
* @param {*} action
* @returns
*/
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A
) {
const lane = requestUpdateLane(fiber);
// 创建一个 update 对象 存放到 fiberNode.updateQueue 对象中
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
// 如果正在处理当前FiberNode的时候触发更新,
// 如 function App() { const [count , setCount ] = useState(1); setCount(2); return <div>111</div> }
// 这时候 setCount(2) 会直接将 update 加入到 fiberNode.updateQueue 中, 然后直接返回
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
// 获取旧节点对象
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// 优化机制:提前计算状态,避免重复渲染
// 队列当前为空,这意味着我们可以在进入渲染阶段之前立即计算下一个状态。
// 如果新状态与当前状态相同,我们或许可以完全退出。不需要触发更新流程了
// 获取 reducer 函数去计算新的状态
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
// 获取旧的状态值
const currentState: S = (queue.lastRenderedState: any);
// 计算出新的状态值
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true;
update.eagerState = eagerState;
// 如果新的状态值和旧的状态值相同,则不需要触发更新流程
if (is(eagerState, currentState)) {
// 虽然不需要重新触发更新,但是仍旧就update 存储到 fiberNode.updateQueue 中
// 这样下次触发更新的时候,就可以获取到最新的 reducer 函数
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
if (__DEV__) {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
}
}
// 将 update 入队,并找到 HostRoot 节点
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// 计算出一个优先级时间戳
const eventTime = requestEventTime();
// 调度更新
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane, action);
}State 的真正计算过程
对于一个 hook 的一次流程中其真正计算 state 的过程不是在 dispatch 的过程中,而是在 useState 的过程中。重新回到上面 mountState 和 updateState 的代码
初次渲染
初次渲染的计算很简单,就是返回 intivitalValue 的值, 这也是我们为什么说 useState(func) 这个 func 只执行一次
更新渲染
在上面我们通过 dispatch 生成了好多 Update 并保存在 hook.pending 上,那么在 updateState 的时候,就会根据这些 Update 去计算出最终的 state,并赋给 hook.memoizedState 。
下面我们按照几个情况去分别讲解计算过程。
有四个 dispatch,分别为 A B C D,其优先级分用 1 2 标识,结果就按照 字符串加的过程进行计算
- A1 -> B1 -> C1 -> D1
这是比较正常的更新流程,其优先级都为 1,其输出结果为 ABCD
在执行 updateReducer 的时候,其 baseQueue === null , pending === A1-> D1,那么会将 current.baseQueue = pendingQueue
然后按照都在 1 的优先级下 执行 newState = reducer(newState, action);
A1 的时候
- newState : "" -> "A"
- newBaseState : null;
- newBaseQueueFirst = null ;
- newBaseQueueLast = null
B1 的时候
- newState : "A" -> "AB"
- newBaseState : null; newBaseQueueFirst = null ; newBaseQueueLast = null
C1 的时候
- newState : "AB" -> "ABC"
- newBaseState : null; newBaseQueueFirst = null ; newBaseQueueLast = null
D1 的时候
- newState : "ABC" -> "ABCD"
- newBaseState : null; newBaseQueueFirst = null ; newBaseQueueLast = null
最后
hook.memoizedState === "ABCD";
hook.baseState === "ABCD"
hook.baseQueue === null
- A1 -> B2 -> C1 -> D2
如果出现不同优先级的 Update,那么因为 updateReduer 的时候会根据isSubsetOfLanes(renderLanes, updateLane) 判断只执行当前优先级下的Update,从而导致 B2 D2不会执行。但是 D2的值又依赖于前面C1的值,为了保持 1. 优先级不同不处理 2. 保持state的值计算的连续性,从而在hook上多了两个属性 baseState 和 baseQueue
baseState
保存了当前 hook 中 update 链表中第一个被跳过的 update 之前 newState 的值 ,在此例子就是 "A"
在下一次优先级执行中可以根据这个值恢复被跳过的 update 之后链表的初始值(下次更新就是从 B2 开始了)
baseQueue
保存了上次优先级中因优先级不同从而跳过执行的第一个 update 及之后的链表,在此例子就是 "B2 -> C1 -> D2" , C1 会再次被执行
下面我们看源码并讲解
A1
- 执行 A1 的时候, baseQueue == null ; pendingQueue ===
A1 -> B2 -> C1 -> D2, 因为 pendingQueue 不为 null 从而 baseQueue === pendingQueue - 执行 do 中 else 的过程,并计算 newState,从而生成下面结果
jsnewState : "" -> "A" newBaseState = null; newBaseQueueFirst = null; newBaseQueueLast = null;- 执行 A1 的时候, baseQueue == null ; pendingQueue ===
B2
- 执行 B2 的时候,发现
!isSubsetOfLanes(renderLanes, updateLane) === true;这时候生成 B2Update 的副本,且因为 newBaseQueueLast === null ,从而使得
jsnewState = "" -> "A" newBaseState = newState = "A" ; newBaseQueueFirst = B2; newBaseQueueLast = B2;- 执行 B2 的时候,发现
C1
- 执行 C1 的时候,发现
!isSubsetOfLanes(renderLanes, updateLane) === false;,又进入计算 newState 的过程,但是这时候与 A1 不同的是 newBaseQueueLast ! == null ; 从而使得 newBaseQueueLast 、newBaseQueueFirst 都进行了修改
jsnewState = "A" -> "AC" newBaseState = newState = "A" ; // 没有进行赋值 newBaseQueueFirst = B2; newBaseQueueLast = C1 -> B2;- 执行 C1 的时候,发现
D2
- 执行 D2 的时候,发现
!isSubsetOfLanes(renderLanes, updateLane) === true;这时候生成 D2Update 的副本,且因为 newBaseQueueLast === null ,从而使得
jsnewState = "AC" -> "AC" newBaseState = newState = "A" ; newBaseQueueFirst = B2; newBaseQueueLast = D2 -> C1 -> B2; // 从而形成了一个从第一个跳过执行的udpate的update环状单向链表- 执行 D2 的时候,发现
在 2 优先级任务执行的时候
这时候 current.baseQueue 不为 null ,current.baseState 也不为 null,那么就将形成一个 baseQueue -> pending 的新的链表(B2 -> C1 -> D2 -> 新的 update),其初始值为 "A"。
这样就保持着 A -> B -> C -> D 的执行顺序,不会因为优先级的不同,导致 setState 的计算顺序产生错误
重点
hook 和 update 的结构
mount 和 update 各自做了什么
如果保持 state 的计算连续性。
始终按照 setState 执行的顺序去进行 state 的计算,不会因为 update 的优先级不同导致 state 的执行顺序产生错误
如何保证 update 变量不会因为优先级问题导致之前 newState 的丢失
这就涉及到 hook 中两个保存 state 值的属性
memoizedState 和 baseStatememoizedState保存了一次 updateState 的初始值 和 最终 newState 的值(最终 newState 的值可能因为优先级问题导致产生错误)baseState保存了第一次因优先级跳过的 update 之前 newState 的值,从而也为下次计算(从跳过开始)的初始值保证了基础
待处理
初始渲染 mountState
js
/**
* useState 初次渲染执行函数
* @param {*} initialState 初始化值
* @returns
*/
function mountState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
// 获取或者创建一个 hook 对象
const hook = mountWorkInProgressHook();
// 支持useState(() => 1) 内容为函数类型
if (typeof initialState === "function") {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
// 将初始化的值赋给 memoizedState 和 baseState
hook.memoizedState = hook.baseState = initialState;
// 生成 updateQueue
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
// 生成 dispatch
const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch =
(dispatchAction.bind(null, currentlyRenderingFiber, queue): any));
// 返回 const [visible, setVisible] = React.useState(true)
//
return [hook.memoizedState, dispatch];
}从源码上可以看出对于 useState 的初次渲染的时候,其声明过程主要是在 workInProgress 上构建 hook 对象,并将 hook 添加到 workInProgress.memoizedState 对象上(如果已经存在 hook,那么就添加到 hook.next 上),从而形成一个 hook 的单项链表结构
然后通过 dispatchAction.bind 提供 setState 的方法,并将参数修改为 dispatchAction(FiberNode , queue , action )
更新渲染 updateState
js
// 就是使用的 updateReducer
function updateState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S
): [S, Dispatch<A>] {
// 复制历史FiberNode的hook,并生成新的hook
const hook = updateWorkInProgressHook();
{
// 计算出新的 newState
hook.memoizedState = newState;
}
return [hook.memoizedState, dispatch];
}从 mountState 和 updateState 这么大的区别可以看出,为什么一个 state 可以在函数式组件中保存自己的状态。其核心就是
- 在初次渲染的时候, 使用 initialState 的值作为 state ,并将当前 hook 缓存到 FiberNode.memoizedState 上
- 在更新渲染的时候,进行 updateState,这时候走不同的逻辑,通过
updateWorkInProgressHook()获取 FiberNode.memoizedState 上对应的 hook,然后根据 Update Queue 计算出最后的结果,并缓存到 hook.memoizedState 上
