跳到主要内容

认识

2023年06月10日
柏拉文
越努力,越幸运

一、认识


React.js Hook 让函数组件也能做类组件的事,有自己的状态,可以处理一些副作用,能获取 ref ,也能做数据缓存。HooksReact.js 中有三种处理策略:

  1. ContextOnlyDispatcher: 第一种形态是防止开发者在函数组件外部调用 hooks ,所以第一种就是报错形态,只要开发者调用了这个形态下的 hooks ,就会抛出异常。

  2. HooksDispatcherOnMount: 第二种形态是函数组件初始化 mount ,因为之前讲过 hooks 是函数组件和对应 fiber 桥梁,这个时候的 hooks 作用就是建立这个桥梁,初次建立其 hooksfiber 之间的关系。

  3. HooksDispatcherOnUpdate: 第三种形态是函数组件的更新,既然与 fiber 之间的桥已经建好了,那么组件再更新,就需要 hooks 去获取或者更新维护状态。

二、细节


2.1 hook.memoizesState

hook.memoizesState 存储 useEffecteffect 副作用链表

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.jsbeginWork 阶段, 遇到 FunctionComponent 类型的 Fiber, 会执行 updateFunctionComponentupdateFunctionComponent 内部调用 renderWithHooks。主要工作如下:

  1. 重置状态: 函数组件执行前, 重置当前正在处理的 hook、当前正在使用的 hook

  2. 记录当前组件 fiber: 通过 currentlyRenderingFiber 变量记录当前组件 fiber

  3. 选择对应阶段的 Hook: mount 阶段选择 HooksDispatcherOnMount 作为 Hooks Map, update 阶段选择 HooksDispatcherOnUpdate 作为 Hooks Map函数组件mount 阶段 创建 hook 数据结构, 存储到 fiber.memoizedState 或者 hook.next, 初次建立其 hooksfiber 之间的关系。函数组件 update 阶段 找到当前函数组件 fiber, 找到当前 hook, 更新 hook 状态。

  4. 执行函数组件: 执行我们真正函数组件,所有的 hooks 将依次执行, 每个 hook 内部要根据 currentlyRenderingFiber 读取对应的内容,

  5. 重置状态: 执行完 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 是否存在。如果不存在, 说明当前 hookhooks 链表的第一个, 所以从 fiber.memoizedState 中取; 如果存在, 直接从 currentHook.next 中取。得到当前 hook 后, 复制当前 hook 形成新的 hooks 链表关系, 更新记录当前使用的 fiber 变量 currentHook 与记录当前正在处理的 fiber 变量 workInProgressHook

由上可以看出, mount 阶段hookupdate 阶段hook 都非常依赖 workInProgressHookcurrentHook 记录、传递 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 指向下一个 hookmount 阶段hookupdate 阶段hook 都非常依赖 workInProgressHookcurrentHook 记录 hooks 链表中的当前正在处理、或者使用的 hook。在 update 阶段更是需要根据 currentHook 拿到当前的 hook, 复制当前 hook, 建立新的 hooks 链表关系。因此, 必须保证 mount 阶段update 阶段 阶段的 hook 执行顺序一致。否则, 有可能在其中一次的渲染中, 因为一个 hook 没有执行, currentHook.next 与 将要执行的 hook 明显是不对应的,造成代码错误。

结论: 凡是使用 currentHookworkInProgressHook 来获取 hookhook, 都必须放在顶部, 绝对不可以在条件和循环里使用, 必须保证 hooks 在每一次渲染中都按照同样的顺序被调用。

6.2 为什么不能在函数组件外部使用 Hooks ?

在函数组件执行之前, React.js 通过 currentlyRenderingFiber 变量记录当前函数组件 fiber。在 mount 阶段hook 内部、update 阶段hook 内部, 都通过 currentlyRenderingFiber 来获取当前函数组件 fiber, 通过 fiber.memoizedState 来获取 hooks 链表。函数组件执行完毕后, 重置 currentlyRenderingFiber