跳到主内容

深入理解React Hooks原理

· 13分钟阅读

React 作为前端最流行的框架之一,引领了前端开发潮流。从 16.8 版本之后推出了 React Hooks,让我们可以通过函数组件就可以来实现 class 组件所有的特性,例如组件状态声明,拥有组件独有的生命周期等等

React Hooks 使用例子

React 官方提供了各种 Hooks,最常用的有 useState 用来声明状态 useEffect用来实现组件钩子,或者触发属性依赖钩子,useCallback用于缓存函数,避免重新创建等,例子如下

Live Editor
Result
Loading...

可以看到函数组件可以拥有自己的状态和生命周期了 😄

为什么要用 Hooks

虽然 class 组件已经能完全满足日常开发需求了,那么为什么 react 还要折腾出新的 Hooks 出来呢?主要原因有如下

方便复用组件的逻辑

在 class 组件的时代,如果要实现组件逻辑复用,有两种方法,通过 renderProps 或者 高阶组件 ,但是随着应用复杂度提升,renderProps 需要重新组织 class 组件代码,高阶组件会导致组件嵌套过深等问题,Hooks 则可以实现无需修改原先组件的结构情况下来达到复用组件状态逻辑。

还有一个问题,class 组件我们会定义多个生命周期,比如 componentDidMount ,componentDidUpdate 然后在里面混入多种逻辑(比如事件监听,数据获取等),这些逻辑无法拆分更细粒度,Hooks 则可以方便将这些逻辑抽离出来成独立的函数,随时随地使用。

使用注意事项

  • 只在顶层使用 hook,不能在循环,条件判断,嵌套函数调用 hook
  • 不能在普通函数调用 hook,不同 hook 可以相互调用

React Fiber

要了解 Hooks 的工作原理,首先得知道 Hooks 使用了一种双向链表的数据结构,每个节点叫做 fiber,一个 fiber 的数据结构如下,这里重点有 returnchildsibling 用来快去获取节点,memoizedState 来存储状态, updateQueue 用来存储更新状态

export type Fiber = {|
// fiber标签,比如有 FunctionComponent,ClassComponent等等
tag: WorkTag,
// 唯一的key
key: null | string,

// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
elementType: any,

// The resolved function/class/ associated with this fiber.
type: any,

// The local state associated with this fiber.
stateNode: any,

// 父节点
return: Fiber | null,
// 子节点
child: Fiber | null,
// 兄弟节点
sibling: Fiber | null,
index: number,

// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
ref: null | (((handle: mixed) => void) & { _stringRef: ?string, ... }) | RefObject,

pendingProps: any, // 当前传入的props
memoizedProps: any, // 上一次的props

// 更新队列,存储状态更新和相关回调
updateQueue: mixed,

// 渲染所需要的状态
memoizedState: any,

// Dependencies (contexts, events) for this fiber, if it has any
dependencies: Dependencies | null,

// Effect
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,

// 方便快速获取下一个effect
nextEffect: Fiber | null,

// 第一个effect
firstEffect: Fiber | null,
// 最后一个effect
lastEffect: Fiber | null,

// 优先级
lanes: Lanes,
childLanes: Lanes,

// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: Fiber | null,
|};

Hooks 内部执行流程

下面介绍一下 useState 内部的执行流程

const [name, setName] = useState("1024nav.com");
const [count, setCount] = useState(0);

useState() 执行内部会获取 dispatcher 对象,这个对象包含 react 自带的所有 Hooks,然后调用 dispatcher.useState() 传入 "1024nav.com",dispatcher.useState() 函数签名如下,最后返回 mountState,首次调用 Hooks 会创建一个 workInProgressHook 的链表,具体查看下面代码

useState: function (initialState) {
currentHookNameInDev = 'useState';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === "function") {
// 如果initState是函数,直接执行再返回
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
});
var dispatch = (queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue));
return [hook.memoizedState, dispatch];
}
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 首次创建Hooks的时候
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 后续的Hooks拼接到链表next
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}

每次 useState 都会执行上面的步骤,最后形的 memoizedState 结构如下

{
"memoizedState": "1024nav.com",
"baseState": "1024nav.com",
"baseQueue": null,
"queue": {
"pending": null,
"lastRenderedReducer": basicStateReducer(state, action),
"lastRenderedState": "1024nav.com"
},
"next": {
"memoizedState": 0,
"baseState": 0,
"baseQueue": null,
"queue": {
"pending": null,
"lastRenderedReducer": basicStateReducer(state, action),
"lastRenderedState": 0
},
"next": null
}
}

上面分析得出,声明状态的时候会创建一个 workInProgressHook 链表,memoizedState 也是以链表形式存储的,那么 setState 的时候是如何执行的

setName("Hello");

当调用 setName('Hello') 的时候,queue 对象会存储 lastRenderedStatedispatchlastRenderedReducerpending,此时的 lastRenderedState 值为 1024nav.compending 是一个循环链表,结构如下

{
memoizedState: {
baseState: "1024nav.com",
memoizedState: "1024nav.com",
pending: {
...
action: 'Hello',
next: {
action: 'Hello',
next: {
action: 'Hello',
next: ...
}
}
}
}
}

可以看出此时的 action 就是我们将要更新的状态值,那么为什么要用循环链表呢?例如我们现在 setName 调用多次

setName("Hello");
setName("World");

上面的 pending 链表就会往头部新增一个节点,这样可以在多次设置值的时候获取到最后的一次操作

{
memoizedState: {
baseState: "1024nav.com",
memoizedState: "1024nav.com",
pending: {
...
action: 'World',
next: {
action: 'Hello',
next: {
action: 'Hello',
next: ...
}
}
}
}
}

然后调用 dispatchAction 来执行更新 state 操作,这里精简代码如下

function dispatchAction(fiber, queue, action) {
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
var update = {
lane: lane,
action: action,
eagerReducer: null,
eagerState: null,
next: null,
}; // 把更新放到链表的尾部
var pending = queue.pending;
if (pending === null) {
// 第一次更新,创建循环链表
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || (alternate !== null && alternate === currentlyRenderingFiber$1)) {
// 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.
var lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
var currentState = queue.lastRenderedState;
var eagerState = lastRenderedReducer(currentState, action); // 临时存储计算更新后的值
// 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;
} catch (error) {
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}

执行 ReactDOMscheduleUpdateOnFiber 来调度更新操作,这时候还没有真正更新

function scheduleUpdateOnFiber(fiber, lane, eventTime) {
var priorityLevel = getCurrentPriorityLevel();
if (lane === SyncLane) {
// 同步更新
schedulePendingInteractions(root, lane);
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
// 如果有回调,执行相关回调
flushSyncCallbackQueue();
}
}
} else {
// Schedule a discrete update but only if it's not Sync.
if (
(executionContext & DiscreteEventContext) !== NoContext && // Only updates at user-blocking priority or greater are considered
// discrete, even inside a discrete event.
(priorityLevel === UserBlockingPriority$2 || priorityLevel === ImmediatePriority$1)
) {
// This is the result of a discrete event. Track the lowest priority
// discrete update per root so we can flush them early, if needed.
if (rootsWithPendingDiscreteUpdates === null) {
rootsWithPendingDiscreteUpdates = new Set([root]);
} else {
rootsWithPendingDiscreteUpdates.add(root);
}
} // Schedule other updates after in case the callback is sync.

ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
}
mostRecentlyUpdatedRoot = root;
}

最后的更新阶段执行 updateReducer 方法,获取 workInProgressHook 然后遍历链表进行更新操作,

function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook();
var queue = hook.queue; // 获取需要更新的队列,更新对应的state
queue.lastRenderedReducer = reducer;
var current = currentHook; // The last rebase update that is NOT part of the base state.
var baseQueue = current.baseQueue; // The last pending update that hasn't been processed yet.
var pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
var baseFirst = baseQueue.next;
var pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}

if (baseQueue !== null) {
// We have a queue to process.
var first = baseQueue.next;
var newState = current.baseState;
var newBaseState = null;
var newBaseQueueFirst = null;
var newBaseQueueLast = null;
var update = first;
do {
var updateLane = update.lane;
// This update does have sufficient priority.
if (newBaseQueueLast !== null) {
var _clone = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: null,
};
newBaseQueueLast = newBaseQueueLast.next = _clone;
} // Process this update.
if (update.eagerReducer === reducer) {
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
newState = update.eagerState;
} else {
var action = update.action;
newState = reducer(newState, action);
}
update = update.next;
} while (update !== null && update !== first);

if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
} // Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!objectIs(newState, hook.memoizedState)) {
// 不相同标记更新
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}

状态 Hooks 多个状态通过链表关联起来,每次状态更新的时候,会通过 update 循环链表存储所有即将更新的操作,在 update 阶段会依次执行更新操作,最后返回最新的 state,这就是为什么不能通过判断来使用 hook,这样会导致链表顺序混乱