10_从 React Hooks 本质看 useState
一、Hooks 的本质
Hooks 是挂在 Fiber 上的一条“有序链表”,通过“调用顺序”来定位状态
每个函数组件对应一个 Fiber:
type Fiber = {
memoizedState: Hook | null; // Hook 链表头
}
对于一个 Hook,有三种类型的 dispatcher(可以认为是操作策略):
/* 函数组件初始化用的 hooks */
// 初始化信息挂载到 fiber 上
const HooksDispatcherOnMount: Dispatcher = {
...
useCallback: mountCallback,
useEffect: mountEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
...
};
/* 函数组件更新用的 hooks */
// 组件更新执行对应的方法,更新 fiber 信息
const HooksDispatcherOnUpdate: Dispatcher = {
...
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
...
};
/* 当 hooks 不是函数组件内部调用或者嵌套 hooks 等“非正确使用”情况,调用这些报错相关的 dispatcher */
const ContextOnlyDispatcher: Dispatcher = {
...
useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
useState: throwInvalidHookError,
...
};
二、Hook 的数据结构
type Hook = {
memoizedState: any; // 当前值
baseState: any;
queue: UpdateQueue | null;
next: Hook | null;
}
注意:这里要和 FiberNode 的 memoizedState 区分开:
- FiberNode.memoizedState:保存的是 Hook 链表里面的第一个链表
- hook.memoizedState:某个 Hook 自身的数据
Fiber.memoizedState
↓
[useState] → [useEffect] → [useMemo] → null
完全依赖调用顺序!
不同的 hook,memoizedState 所存储的内容不同:
- useState:对于
const [state, updateState] = useState(initialState),memoizedState 保存的是 state 的值 - useReducer:对于
const [state, dispatch] = useReducer(reducer, { } ),memoizedState保存的是 state 的值 - useEffect:对于
useEffect( callback, [...deps] ),memoizedState 保存的是callback、[...deps]等数据 - useRef:对于
useRef(initialValue),memoizedState保存的是{ current: initialValue} - useMemo:对于
useMemo( callback, [...deps] ),memoizedState 保存的是[callback( )、[...deps]]数据 - useCallback:对于 u
seCallback( callback, [...deps] ),memoizedState 保存的是[callback、[...deps]]数据 - useContext:不需要 memoizedState 保存自身数据
三、执行流程(mount 阶段)
1️⃣ render 开始
function Component() {
const [count, setCount] = useState(0);
}
// render 开始先执行
export function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
// 每一次执行函数组件之前,先清空 FiberNode 状态 (用于存放 hooks 列表)
workInProgress.memoizedState = null;
// 清空更新队列(用于存放 effect 列表)
workInProgress.updateQueue = null;
// ...
// 根据不同的组件状态初始化不同的 dispatcher 对象和上下文
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 执行函数组件,所有的 hooks 将依次执行
let children = Component(props, secondArg);
// ...
// 兜底
finishRenderingHooks(current, workInProgress);
return children;
}
function finishRenderingHooks(current, workInProgress) {
// 防止 hooks 在不合规的情况下调用,如果调用直接报错
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
// ...
}
2️⃣ mountState 做了什么
如果组件是挂载阶段:
function mountWorkInProgressHook() {
const hook = {
memoizedState: null, // Hook 自身的状态
baseState: null,
baseQueue: null,
queue: null, // hook 自身队列
next: null, // next 指向下一个 hook
};
// 判断当前的 hook 是否是链表的第一个
if (workInProgressHook === null) {
// 如果当前组件的 Hook 链表为空,那么就将刚刚新建的 Hook 作为 Hook 链表的第一个节点(头结点)
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 果当前组件的 Hook 链表不为空,那么就将刚刚新建的 Hook 添加到 Hook 链表的末尾(作为尾结点)
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
function mountStateImpl(initialState) {
// 获取 hook 对象
const hook = mountWorkInProgressHook();
//...
// 初始化 memoizedState
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer, // useState 内置的 reducer
lastRenderedState: (initialState: any),
};
// 初始化 queue
hook.queue = queue;
return hook;
}
function mountState(initialState) {
// 获取 hook 对象
const hook = mountStateImpl(initialState);
const queue = hook.queue;
const dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
));
// 初始化 dispatch (dispatch 就是用来修改状态的方法)
queue.dispatch = dispatch;
// 返回 [当前状态, dispatch函数]
return [hook.memoizedState, dispatch];
}
其实 useReducer 和 useState 非常像,在源码层面:
- mount 阶段:mountState 和 mountReducer 的大体流程是一样的。但是有一个区别,mountState 的 queue 里面的 lastRenderedReducer 对应的是 basicStateReducer,而 mountReducer 的 queue 里面的 lastRenderedReducer 对应的是开发者自己传入的 reducer,这里说明了一个问题,useState 的本质就是 useReducer 的一个简化版,只不过在 useState 内部,会有一个内置的 reducer
- update 阶段:在 update 阶段,updateState 内部直接调用的就是 updateReducer,传入的 reducer 仍然是 basicStateReducer。
function mountReducer(reducer, initialArg, init) { // 创建 hook 对象 const hook = mountWorkInProgressHook(); let initialState; // 如果有 init 初始化函数,就执行该函数,并将执行的结果赋值给 initialState if (init !== undefined) { initialState = init(initialArg); } else { initialState = initialArg; } // 赋值给 hook 对象的 memoizedState hook.memoizedState = hook.baseState = initialState; const queue = { pending: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: reducer, // 手动传入的 reducer lastRenderedState: initialState, }; hook.queue = queue; const dispatch = (queue.dispatch = dispatchReducerAction.bind( null, currentlyRenderingFiber, queue )); return [hook.memoizedState, dispatch]; }
3️⃣ 构建 Hook 链表
第一次 render:
Fiber.memoizedState → Hook1 → Hook2 → Hook3
举个例子~
该示例来源:渡一教育。
function App() {
const [number, setNumber] = React.useState(0); // 第一个hook
const [num, setNum] = React.useState(1); // 第二个hook
const dom = React.useRef(null); // 第三个hook
React.useEffect(() => {
// 第四个
hookconsole.log(dom.current);
}, []);
return (
<div ref={dom}>
<div onClick={() => setNumber(number + 1)}> {number} </div>
<div onClick={() => setNum(num + 1)}> {num}</div></div>
);
}
四、更新阶段(update)
不再创建 Hook,而是“复用”
function updateWorkInProgressHook(){
let nextCurrentHook: null | Hook;
if (currentHook === null) {
// 从 alternate 上获取到 fiber 对象
const current = currentlyRenderingFiber.alternate;
// 获取第一个 hook
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 获取下一次 hook
nextCurrentHook = currentHook.next;
}
// workInProgressHook 会指向下一个要工作的 hook
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// 已经存在,直接复用
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
// 如果 nextWorkInProgressHook 不为 null,那么就会复用之前的 hook
// 划重点!!!
// 更新的过程中,如果通过条件语句增加或者删除了 hook,复用的时候就会产生当前 hook 的顺序和之前 hook 的顺序不一致的问题
if (nextCurrentHook === null) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
// This is the initial render. This branch is reached when the component
// suspends, resumes, then renders an additional hook.
// Should never be reached because we should switch to the mount dispatcher first.
throw new Error(
'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
);
} else {
// This is an update. We should always have a current hook.
throw new Error('Rendered more hooks than during the previous render.');
}
}
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
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;
}
function updateReducer() {
const hook = updateWorkInProgressHook();
return updateReducerImpl(hook, ((currentHook)), reducer);
}
function updateState<S>(initialState) {
return updateReducer(basicStateReducer, initialState);
}
接着上面的示例~
示例来源:渡一教育。
function App({ showNumber }) {
let number, setNumber
showNumber && ([ number,setNumber ] = React.useState(0)) // 第一个hooks
const [num, setNum] = React.useState(1); // 第二个hook
const dom = React.useRef(null); // 第三个hook
React.useEffect(() => {
// 第四个hook
console.log(dom.current);
}, []);
return (
<div ref={dom}>
<div onClick={() => setNumber(number + 1)}> {number} </div>
<div onClick={() => setNum(num + 1)}> {num}</div></div>
);
}
假设第一次父组件传递过来的 showNumber 为 true,此时就会渲染第一个 hook;第二次渲染的时候,假设父组件传递过来的是 false,那么第一个 hook 就不会执行,那么逻辑就会变得:
第一次:useState -> useState
第二次:useState -> useRef
体现在我们开发者眼中就是报错。
五、setState 到底做了什么?
dispatch 流程
function dispatchSetState(action) {
const update = {
action,
next: null
};
enqueueUpdate(queue, update);
scheduleUpdateOnFiber(fiber);
}
UpdateQueue 结构
hook.queue
↓
update1 → update2 → update3(环形链表)
执行更新
function processUpdateQueue(queue) {
let state = baseState;
queue.forEach(update => {
state = reducer(state, update.action);
});
return state;
}
六、调度机制(Hooks 如何触发更新)
scheduleUpdateOnFiber(fiber)
setState
↓
scheduleUpdate
↓
标记 lane(优先级)
↓
render(可中断)
↓
commit(不可中断)
Hooks 如何保证并发下的 hooks 行为正确?
关键:
- 每次 render 都重新走一遍 Hook 链
- 不依赖“执行次数”,只依赖“顺序”