大家好,我是前端西瓜哥。
专业成都网站建设公司,做排名好的好网站,排在同行前面,为您带来客户和效益!创新互联为您提供成都网站建设,五站合一网站设计制作,服务好的网站设计公司,网站制作、成都网站制作负责任的成都网站制作公司!
今天我们从源码来理解 React Hook 是如何工作的。
React Hook 是 React 16.8 后新加入的黑魔法,让我们可以 在函数组件内保存内部状态。
Hook 的优势:
在讲解源码之前,先认识一些 重要的全局变量:
currentlyRenderingFiber:正在处理的函数组件对应 fiber。在执行 useState 等 hook 时,需要通过它知道当前 hook 对应哪个 fiber。
workInProgressHook:挂载时正在处理的 hook 对象。我们会沿着 workInProcess.memoizedState 链表一个个往下走,这个 workInProgressHook 就是该链表的指针。
currentHook:旧的 fiber 的 hooks 链表(current.memorizedState)指针。
ReactCurrentDispatcher:全局对象,是一个 hook 调度器对象,其下有 useState、useEffect 等方法,是我们业务代码中 hook 底层调用的方法。ReactCurrentDispatcher 有三种:
构建函数实例是在 renderWithHooks 方法中进行的。
主要逻辑为:
function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderLanes
) {
renderLanes = nextRenderLanes;
// 1. 将 workInProgress 赋值给全局变量 currentlyRenderingFiber
// 这样我们在调用 Hook 时就能知道对应的 fiber 是谁
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// 2. 根据是挂载还是更新阶段,选择对应 hook 调度器
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 3. 调用函数组件,里面执行各种 React Hook,并返回 ReactElement
let children = Component(props, secondArg);
// 4. hook 调度器还原为 ContextOnlyDispatcher
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
// 将一些全局变量进行重置
renderLanes = NoLanes;
currentlyRenderingFiber = null;
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
// Hook 数量比上次少,对不上,报错
if (didRenderTooFewHooks) {
throw new Error(
'Rendered fewer hooks than expected. This may be caused by an accidental ' +
'early return statement.',
);
}
return children;
}
下面看看在函数组件一些常见 Hook 是如何工作的。
首先讨论 状态 Hook 中最常见的一种:useState。
useState 在挂载阶段,调用的是 HooksDispatcherOnMount.useState,也就是 mountState。
function mountState(initialState) {
// 1. 创建一个 hook 对象,并添加到 workInProcess.memoizedState 链表上
const hook = mountWorkInProgressHook();
// useState 传入的可能是个函数,要调用一下拿到初始值
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;
// 更新 state 的方法
const dispatch = queue.dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
);
// 返回我们经常用的 [state, setState]
return [hook.memoizedState, dispatch];
}
mountWorkInProgressHook 实现:
function mountWorkInProgressHook() {
// 新的 hook 空对象
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
// 给 memoizedState 链表加节点的逻辑
// 写过单链表的会比较理解,头节点要特殊处理
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
之前 mountState 时,我们返回了一个绑定了 fiber、queue 参数的 dispatchSetState。setState 更新操作调用的正是这个 dispatchSetState。
第一个 setState 在被调用时会立即计算新状态,这是为了 做新旧 state 对比,决定是否更新组件。如果对比发现状态没变,继续计算下一个 setState 的新状态,直到找到为止。如果没找到,就不进行更新。
其后的 setState 则不会计算,等到组件重新 render 再计算。
为对比新旧状态计算出来的状态值,会保存到 update.eagerState,并将 update.hasEagerState 设置为 true,之后更新时通过它来直接拿到计算后的最新值。
dispatchSetState 会拿到对应的 fiber、queue(对应 hook 的 queue)、action(新的状态)。
function dispatchSetState(fiber, queue, action) {
const lane = requestUpdateLane(fiber);
// 创建一个 update 更新对象
const update = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: null,
};
if (isRenderPhaseUpdate(fiber)) {
// 渲染阶段更新,先不讨论这种特殊情况
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
if (
// 第二次 setState 时,fiber.lanes 为 SyncLane
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState = queue.lastRenderedState;
// 计算新状态
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// 对比新旧状态是否不同
if (is(eagerState, currentState)) {
// 状态没改变,当前 setState 无效,return 结束,无事发生
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
}
}
}
// 将 update 加到 queue 链表末尾
// 将 fiber 标记为 SyncLane
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
// 调度 fiber 更新
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}
我们先了解一个前置知识:useState 是特殊的 useReducer。
useState 本质上在使用 useReducer,在 React 源码层提供了特殊的名为 basicStateReducer 的 reducer,后面源码解析中会看到它。
const _useState = (initalVal) => {
return React.useReducer(
function (preState, action) {
// action 对应 setState 传入的最新状态
// 如果不是函数,直接更新为最新状态
// 如果是函数,传入 preState 并调用函数,并将返回值作为最新状态
return typeof action === 'function' ? action(preState) : action;
},
initalVal
)
}
回到正题。
useState 在更新阶段会拿到上一次的状态值,此阶段调用的是 HooksDispatcherOnUpdate.useState,也就是 updateState。
updateState 会调用 updateReducer(useReducer 更新阶段也用这个),这也是为什么我说 setState 是特殊 useReducer 的原因。
updateReducer 主要工作有两个:
function updateState(initialState) {
// 实际用的是 updateReducer
return updateReducer(basicStateReducer);
}
// reducer 函数
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
// setReducer 更新阶段对应的 updateReducer
function updateReducer(reducer, initialArg, init) {
// ----- 【1】 拷贝 hook(current -> workInProcess),并返回这个 hook -----
const hook = updateWorkInProgressHook();
// ----- 【2】 读取队列,计算出最新状态,更新 hook 的状态 -----
// ...
}
先看看 updateWorkInProgressHook 方法。
该方法中,currentHook 设置为 current.memoizedState 链表的下一个 hook,拷贝它到 currentlyRenderingFiber.memoizedState 链表上,返回这个 hook。
function updateWorkInProgressHook() {
// 1. 移动 currentHook 指针
//(来自 current.memoizedState 链表)
var nextCurrentHook;
if (currentHook === null) {
var current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
// 2. 移动 workInProgressHook 指针
//(来自 currentlyRenderingFiber.memoizedState 链表)
var nextWorkInProgressHook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// 这种情况为 “渲染时更新逻辑”(在 render 时调用了 setState)
// 为了更聚焦普通情况,这里不讨论
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// 3. 渲染时不更新,nextWorkInProgressHook 就一定是 null
if (nextCurrentHook === null) {
throw new Error('Rendered more hooks than during the previous render.');
}
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null // next 就不拷贝了
};
// 4. 经典单链表末尾加节点写法
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
// 5. 返回拷贝 hook 对象
return workInProgressHook;
}
拿到拷贝后的 hook,就可以计算新状态值了。
首先将 hook.queue.pending 队列合并到 currentHook.baseQueue 下。该队列包含了一系列 update 对象(因为可能调用了多次 setState),里面保存有 setState 传入的最新状态值(函数或其他值)。
然后遍历 update 计算出最新状态,保存回 hook,并返回最新状态值和 setState 方法。
function updateReducer(reducer, initialArg, init) {
// ----- 【1】 拷贝 hook(current -> workInProcess),并返回这个 hook ----
const hook = updateWorkInProgressHook();
// ----- 【2】 读取队列,计算出最新状态,更新 hook 的状态 -----
// 取出 hook.queue 链表,添加到 current.baseQueue 末尾
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current = currentHook;
let baseQueue = current.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 处理更新队列
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
// 循环,根据 baseQueue 链表下的 update 对象计算新状态
do {
// 删掉了一些跳过更新的逻辑
if (update.hasEagerState) {
// 为了对比新旧状态来决定是否更新,所计算的新状态。
// 如果不同,给 update.hasEagerState 设置为 true
// 新状态赋值给 update.eagerState
newState = update.eagerState;
} else {
// 计算新状态
const action = update.action;
newState = reducer(newState, action);
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 更新 hook 状态
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
有些逻辑类似 useState,比如创建 hook 的 mountWorkInProgressHook 方法实现,所以一些重复逻辑就不说了,直奔核心。
核心函数是 mountEffectImpl。
mountEffectImpl(fiberFlags, hookFlags, create, deps) {
// create 和 deps 是 useEffect 接受的两个参数
// 1. 新建 hook 对象
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
// 2. 新建 effect 对象,放到 hook.memoizedState 下。
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
pushEffect 实现:
function pushEffect(tag, create, destroy, deps) {
// 创建 effect
var effect = {
tag: tag,
create: create,
destroy: destroy,
deps: deps,
next: null
};
var componentUpdateQueue = currentlyRenderingFiber.updateQueue;
// 添加到当前 fiber.updateQueue 下。
// updateQueue.laseEffect 保存链表的最后一个 effect
// 且使用的是环形链表,通过 updateQueue.laseEffect.next 得到链表头节点
// 如果 updateQueue 为 null,初始化一个空的 updateQueue 对象
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 往 updateQueue.lastEffect 链表上添加 effect 对象。
var lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
核心实现在 updateEffectImpl。
function updateEffect(create, dep) {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
// hookFlags 此时为 PassiveEffect(代表)
// 1. 从 current 拷贝 hook 到 workInProcess
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
// 存在依赖项
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 依赖项没有改变,结束
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 还是会新建 effect,更新 updateQueue 和 memorizedState
// 但 tag 只是 PassiveEffect,后面遍历时不会执行
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 当前 fiber 打上 PassiveEffect 标记
// 该标记表示存在需要执行的 useEffect
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
// 相比上面依赖项不变的情况,这里加了 HookHasEffect 标签
// 之后根据 fiber.updateQueue 会执行
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
我们看下依赖项对比算法 areHookInputsEqual 的细节,它同时遍历到新旧依赖项最长的尾部,进行 Object.is 对比。在空数组情况下,这个比较一定返回 true,所以能模拟 componentDidMount / Unmount 的效果。
function areHookInputsEqual(nextDeps, prevDeps) {
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
当 commit 阶段结束后,useEffect 的 create 和 destroy 会被 Schedule 调度器异步调度执行。
fiber.updateQueue 下的 effect 会按顺序取出,然后一个个执行。
function commitPassiveUnmountOnFiber(finishedWork) {
// 执行所有 tag 为 HookPassive | HookHasEffect 的 effect 的 destroy
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
finishedWork,
finishedWork.return,
);
// 执行所有 tag 为 HookPassive | HookHasEffect 的 effect 的 create
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
finishedWork,
finishedWork.return,
);
}
之前依赖项相同的话,虽然也创建 effect,但它的 tag 对不上,是不会执行的。
1、React Hooks 为什么不能写在条件语句中?
我们要保证 React Hooks 的顺序一致。
函数组件的状态是保存在 fiber.memorizedState 中的。它是一个链表,保存调用 Hook 生成的 hook 对象,这些对象保存着状态值。当更新时,我们每调用一个 Hook,其实就是从 fiber.memorizedState 链表中读取下一个 hook,取出它的状态。
如果顺序不一致了或者数量不一致了,就会导致错误,取出了一个其他 Hook 对应的状态值。
2、React Hooks 为什么必须在函数组件内部执行?React 如何能够监听 React Hooks 在外部执行并抛出异常?
Hooks 底层调用的是一个全局变量 ReactCurrentDispatcher 的一系列方法。
这个全局变量会在不同阶段设置为不同的对象。render 过程中,挂载阶段设置为 HooksDispatcherOnMount,更新阶段设置为 HooksDispatcherOnUpdate。它们会读取 currentlyRenderingFiber 全局变量,这个全局变量代表正在处理的 fiber,读取它进行一些设置状态和读取状态等操作。
在 render 阶段外,会设置为 ContextOnlyDispatcher,这个对象下所有方法都会抛出错误,因为此时不存在正常处理的 fiber,使用时机是并不对。
本文只讲了状态 Hook 代表 useState,和 副作用 Hook 代表 useEffect,其他 Hook 其实也差不多。
分享标题:从源码理解ReactHook是如何工作的
分享地址:http://www.36103.cn/qtweb/news31/12931.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联