认识
一、认识
React.js Hook
让函数组件也能做类组件的事,有自己的状态,可以处理一些副作用,能获取 ref
,也能做数据缓存。Hooks
在 React.js
中有三种处理策略:
-
ContextOnlyDispatcher
: 第一种形态是防止开发者在函数组件外部调用hooks
,所以第一种就是报错形态,只要开发者调用了这个形态下的hooks
,就会抛出异常。 -
HooksDispatcherOnMount
: 第二种形态是函数组件初始化mount
,因为之前讲过hooks
是函数组件和对应fiber
桥梁,这个时候的hooks
作用就是建立这个桥梁,初次建立其hooks
与fiber
之间的关系。 -
HooksDispatcherOnUpdate
: 第三种形态是函数组件的更新,既然与fiber
之间的桥已经建好了,那么组件再更新,就需要hooks
去获取或者更新维护状态。
二、细节
2.1 hook.memoizesState
hook.memoizesState
存储 useEffect
的 effect
副作用链表
hook.memoizedState = pushEffect(
Passive | HookHasEffect,
create,
undefined,
nextDeps
);
2.2 fiber.memoizedState
fiber.memoizedState
存储 Hooks
链表
if (workInProgressHook == null) {
if (currentlyRenderingFiber == null) {
throw new Error('请在函数组件内调用 Hook');
} else {
workInProgressHook = hook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
workInProgressHook.next = hook;
workInProgressHook = hook;
}
三、执行函数组件
在 React.js
的 beginWork
阶段, 遇到 FunctionComponent
类型的 Fiber
, 会执行 updateFunctionComponent
。updateFunctionComponent
内部调用 renderWithHooks
。主要工作如下:
-
重置状态: 函数组件执行前, 重置当前正在处理的
hook
、当前正在使用的hook
-
记录当前组件
fiber
: 通过currentlyRenderingFiber
变量记录当前组件fiber
-
选择对应阶段的
Hook
:mount
阶段选择HooksDispatcherOnMount
作为Hooks Map
,update
阶段选择HooksDispatcherOnUpdate
作为Hooks Map
。 函数组件mount
阶段 创建hook
数据结构, 存储到fiber.memoizedState
或者hook.next
, 初次建立其hooks
与fiber
之间的关系。函数组件update
阶段 找到当前函数组件fiber
, 找到当前hook
, 更新hook
状态。 -
执行函数组件: 执行我们真正函数组件,所有的
hooks
将依次执行, 每个hook
内部要根据currentlyRenderingFiber
读取对应的内容, -
重置状态: 执行完
Component
, 立马重置currentlyRenderingFiber
, 防止函数组件外部调用。置当前正在处理的hook
、当前正在使用的hook
3.1 /packages/react-reconciler/fiberHooks.js
export function renderWithHooks(workInProgress, Component, lane) {
renderLane = lane;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
const current = workInProgress.alternate;
if (current !== null) {
// update
currentDispatcher.current = HooksDispatcherOnUpdate;
} else {
// mount
currentDispatcher.current = HooksDispatcherOnMount;
}
const props = workInProgress.pendingProps;
const children = Component(props);
currentHook = null;
renderLane = NoLane;
workInProgressHook = null;
currentlyRenderingFiber = null;
return children;
}
3.2 /packages/react-reconciler/fiberHooks.js
/**
* @description: Mount 阶段 Hooks 实现
*/
const HooksDispatcherOnMount = {
use,
useRef: mountRef,
useMemo: mountMemo,
useState: mountState,
useEffect: mountEffect,
useContext: readContext,
useCallback: mountCallback,
useTransition: mountTransition
};
/**
* @description: Update 阶段 Hooks 实现
*/
const HooksDispatcherOnUpdate = {
use,
useRef: updateRef,
useMemo: updateMemo,
useState: updateState,
useEffect: updateEffect,
useContext: readContext,
useCallback: updateCallback,
useTransition: updateTransition
};
四、函数组件 mount 阶段 Hook
在 mount
阶段, 执行 mountWorkInProgressHook
创建新的 hook
数据结构, 存储到 fiber.memoizedState
或者 hook.next
, 与当前函数组件 fiber
建立联系, 并更新记录当前正在处理的 hook
变量 workInProgressHook
。
function mountWorkInProgressHook() {
const hook = {
next: null,
baseQueue: null,
baseState: null,
updateQueue: null,
memoizedState: null
};
if (workInProgressHook == null) {
if (currentlyRenderingFiber == null) {
throw new Error('请在函数组件内调用 Hook');
} else {
workInProgressHook = hook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
workInProgressHook.next = hook;
workInProgressHook = hook;
}
return workInProgressHook;
}
五、函数组件 update 阶段 Hook
在 update
阶段, 执行 updateWorkInProgressHook
, 从 currentlyRenderingFiber
记录当前函数组件 fiber
中取出对应的 hook
。取出时是判断当前正在使用的 hook
变量 currentHook
是否存在。如果不存在, 说明当前 hook
是 hooks
链表的第一个, 所以从 fiber.memoizedState
中取; 如果存在, 直接从 currentHook.next
中取。得到当前 hook
后, 复制当前 hook
形成新的 hooks
链表关系, 更新记录当前使用的 fiber
变量 currentHook
与记录当前正在处理的 fiber
变量 workInProgressHook
。
由上可以看出, mount
阶段 的 hook
和 update
阶段 的 hook
都非常依赖 workInProgressHook
和 currentHook
记录、传递 hooks
链表中的当前正在处理、或者使用的 hook
, 因此, 必须保证 mount
阶段 和 update
阶段 阶段的 hook
执行顺序一致。
function updateWorkInProgressHook() {
let nextCurrentHook = null;
if (currentHook == null) {
const current = currentlyRenderingFiber?.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
if (nextCurrentHook == null) {
/**
* @description: nextCurrentHook 为 null
* 之前: u1 u2 u3
* 现在: u1 u2 u3 u4
* 原因:
* 1. if(){ u4 } => u4 放在了 If 语句中
*/
throw new Error('本次执行时的 Hook 比上次执行时多');
}
currentHook = nextCurrentHook;
const newHook = {
next: null,
baseState: currentHook.baseQueue,
baseQueue: currentHook.baseState,
updateQueue: currentHook.updateQueue,
memoizedState: currentHook.memoizedState
};
if (workInProgressHook == null) {
if (currentlyRenderingFiber == null) {
throw new Error('请在函数组件内调用 Hook');
} else {
workInProgressHook = newHook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
workInProgressHook.next = newHook;
workInProgressHook = newHook;
}
return workInProgressHook;
}
六、沉淀与思考
6.1 为什么不能在条件和循环里使用Hooks
?
const hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: {
pending:null
},
next: null,
};
hooks
是一个单向链表, next
指向下一个 hook
。 mount
阶段 的 hook
和 update
阶段 的 hook
都非常依赖 workInProgressHook
和 currentHook
记录 hooks
链表中的当前正在处理、或者使用的 hook
。在 update
阶段更是需要根据 currentHook
拿到当前的 hook
, 复制当前 hook
, 建立新的 hooks
链表关系。因此, 必须保证 mount
阶段 和 update
阶段 阶段的 hook
执行顺序一致。否则, 有可能在其中一次的渲染中, 因为一个 hook
没有执行, currentHook.next
与 将要执行的 hook
明显是不对应的,造成代码错误。
结论: 凡是使用 currentHook
、 workInProgressHook
来获取 hook
的 hook
, 都必须放在顶部, 绝对不可以在条件和循环里使用, 必须保证 hooks
在每一次渲染中都按照同样的顺序被调用。
6.2 为什么不能在函数组件外部使用 Hooks
?
在函数组件执行之前, React.js
通过 currentlyRenderingFiber
变量记录当前函数组件 fiber
。在 mount
阶段 的 hook
内部、update
阶段 的 hook
内部, 都通过 currentlyRenderingFiber
来获取当前函数组件 fiber
, 通过 fiber.memoizedState
来获取 hooks
链表。函数组件执行完毕后, 重置 currentlyRenderingFiber
。