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的 单向链表结构
hooks的数据状态如何保存
上面例子中,在App组件初始渲染的时候,我们知道state的值为 useState(1)的初始化值 1,那么在effect执行后更新到2、3的时候,这个时候函数式组件又会从上到下执行,那么这时候为什么React知道num的值是3 而不是 初始化值 1呐?
这就涉及到useState的值是如何保存和读取的,如果在初次渲染的时候使用初始化值,在更新渲染的时候使用新的状态值。
下面我们看一下 renderWithHooks中的部分代码
js
export function renderWithHooks<Props, SecondArg>(...): any {
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// 这就是 React提供的Hooks方法的执行函数
// 其在组件初始化渲染的时候 使用 HooksDispatcherOnMount里面的方法
// 在 组件更新渲染的时候 使用 HooksDispatcherOnUpdate里面的方法
// 如 useState 在 初始化的时候使用 mountState 在更新渲染的时候使用 updateState
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 执行函数组件方法
let children = Component(props, secondArg);
// 重置
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
currentHook = null;
workInProgressHook = null;
return children;
}
ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;
通过 current === null || current.memoizedState === null 去区分函数式组件当前渲染是初次渲染还是更新渲染
然后根据不同的组件状态去通过不同的Hooks函数 执行方法
初始渲染 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 上
上面我们知道了在不同的生命周期获取正确的state,那么其怎么去保存这些数据的?
这就涉及到 Hook的结构的缓存,
hooks的保存结构
在初次渲染的时候,有一个关键的函数 mountWorkInProgressHook()
,这就是React的hook在组件初次渲染的时候,如果通过workInProgressHook
的全局变量去按照组件函数执行的顺序生成对应的 hook单向链表结构。
在组件更新渲染的情况下, 其调用的函数变成了updateWorkInProgressHook()
,这个函数不是如何去创建hook并生成对应的 hook单向链表结构,而是根据相同的顺序去获取 mount上面的此下标下的hook进行复用,并构建新的 memoizedState
mountWorkInProgressHook()
hook对象的创建
js
/**
* beginWork 初次渲染的时候 有一个Hooks就生成一个 hook对象,然后按照链表的结构保存到 workInProgreee.memoizedState上
* 然后再将workInProgressHook指向当前的 hook对象
* @returns
*/
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
// 用于保存一次更新渲染流程的初始值 --- 对应的是 render的值
memoizedState: null,
// 用于保存一次渲染流程中多个update的值
baseState: null,
baseQueue: null,
// 保存了 update 更新链
queue: null,
// 指向下一个hook
next: null,
};
// 如果当前函数组件第一次初始化 Hooks
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
updateWorkInProgressHook()
js
/**
* 函数式组件
* 组件更新渲染的时候,执行到 useState等Hook 按照新FiberNode.alternate(久的FiberNode)的链表去在FiberNode.memoizedState上复制hook链;
* @returns
*/
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
// currentHook为上一个Hook
// 1. 如果 currentHook === null 说明这是处理的第一个 hook 那就通过 FiberNode.alternate.memoizedState 获取旧的FiberNode的第一个hook
// 2. 如果 currentHook !== null 说明这是第二个及后面的 这时候就通过 currentHook nextCurrentHook 获取到当前hook在初次渲染的时候创建的 hook对象内容
if (currentHook === null) {
// 获取FiberNode 的 原来的FiberNode
const current = currentlyRenderingFiber.alternate;
// 有久的FiberNode
if (current !== null) {
// 那就将 nextCurrentHook 指向第一个 hook
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 将上一个 Hook.next 作为当前的 hook
nextCurrentHook = currentHook.next;
}
// 跟 currentHook、nextCurrentHook的逻辑一样
// 通过 workInProgressHook 、nextWorkInProgressHook
// 在新的FiberNode.memoizedState上按照 nextCurrentHook的数据生成新的hook单向链表结构
// 注意:
// 1. nextWorkInProgressHook 为 null
// 生成新的hook(newHook)的时候,其newHook.next === null
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
// 将next作为当前处理的 hook
currentHook = nextCurrentHook;
// 生成一个新的 hook
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
// 将新的hook单向链表添加到新的FiberNode.memoizedState上
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
hook结构
memoizedState
用于保存一次更新渲染流程的初始值 --- 对应的是 render 的值
baseState
用于保存因为优先级不同导致Update跳过之前计算的最终state的值(newState),如下述栗子的 "A",
从而为下次计算不会因为之前的update不再执行,从而导致state的值丢失 (ABCD不会变成 BCD)
baseQueue
保存因为优先级不同导致Update不执行的第一个跳过的链表,在下次执行的时候作为baseQueue的头部
queue
保存了此次触发中产生的Update,用于在组件render的时候根据update的链表计算出最终的值
next
指向下一个hook, 从而形成一个 hook的单向链表结构
触发修改
上文都是说的如何在初次渲染或者更新渲染的时候 如何创建 hook单向链表、复用hook单项链表和获取state
下面我们主要看其第二个值 触发修改
dispatchAction
需要注意的是,dispatchAction 不是我们想象的怎么去计算出新的 state,其只是根据你的dispatch去创建一个Update对象,然后在组件更新渲染的时候,根据update queue去计算出真正的值
其主要逻辑如下:
生成Update
将Update加入到hook.pending 环状单向链表中去
是否触发更新(
scheduleUpdateOnFiber(fiber, lane, eventTime)
)注意: 这里是触发更新,不是更新组件
- 如果在当前函数组件function执行的的过程中触发更新,那就直接跳过触发更新
- 如果是最高优先级的更新且前后值相同 那么也跳过触发更新
最后如果我们一个事件中进行了多次的 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 dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
if (__DEV__) {
if (typeof arguments[3] === 'function') {
console.error(
"State updates from the useState() and useReducer() Hooks don't support the " +
'second callback argument. To execute a side effect after ' +
'rendering, declare it in the component body with useEffect().',
);
}
}
// 获取事件的时间
const eventTime = requestEventTime();
// 获取优先级
const lane = requestUpdateLane(fiber);
// 生成Update对象 ,下面就是构建这个对象
const update: Update<S, A> = {
lane,
// 计算的方法或者值 setNum((num) => num +1) -> num => num + 1
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// Append the update to the end of the list.
// 将Update添加到hook.pending中,并通过next形成一个单项圆环链表结构
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
// 获取原来的FiberNode
const alternate = fiber.alternate;
if (
// 触发更新的fiber === 当前渲染的 fiber
// 如 我们在渲染函数中直接调用 setState() function App(){ const [num, setNum] = React.useState(1) ; setNum(2); return xxx }
// 那么就不添加了
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
} else {
// 如果是一个最高优先级的更新
// 那么直接判断是否需要更新
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
// 上次的执行dispatch
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
if (__DEV__) {
prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
const currentState: S = (queue.lastRenderedState: any);
// 计算出新的state
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.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
// 如果值没有改变,那就不触发更新
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
if (__DEV__) {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
}
}
if (__DEV__) {
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfNotScopedWithMatchingAct(fiber);
warnIfNotCurrentlyActingUpdatesInDev(fiber);
}
}
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
if (__DEV__) {
if (enableDebugTracing) {
if (fiber.mode & DebugTracingMode) {
const name = getComponentName(fiber.type) || 'Unknown';
logStateUpdateScheduled(name, lane, action);
}
}
}
if (enableSchedulingProfiler) {
markStateUpdateScheduled(fiber, lane);
}
}
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 和 baseState
memoizedState
保存了一次updateState的初始值 和 最终newState的值(最终newState的值可能因为优先级问题导致产生错误)baseState
保存了第一次因优先级跳过的update之前newState的值,从而也为下次计算(从跳过开始)的初始值保证了基础