前言
基于前面几篇的解析,我们已经了解了 react
渲染的主链路了,当然还有 commit
阶段没有详细的解析,后面会补充。这一篇我们主要来看看,hook
是实现更新组件状态的,当然除了状态hook
,还有副作用hook
,这个我们放到后面再继续介绍。
背景
在解析原理之前,我们先来思考一下,hook
的引入是为了解决什么问题的,这一点很重要。
- 在原有的状态组件实现上,只能基于复杂的
class
组件来实现。 - 复用状态逻辑只能基于高阶组件和
props
传递,导致组件嵌套过深。
hooks使用
我们来看看 hook
是如何使用的。
js
import React, { useState } from 'react'
export default function hookState() {
const [hookState, setHookState] = useState(0)
return (
<>
<div>
count: {hookState}
</div>
<button onClick={() => {
setHookState(preState => {
return preState + 1
})
}}>
点我
</button>
</>
)
}
这样简单的方式就可以实现在函数组件中,保持状态了。
hook实现猜想
基于我们之前对react
渲染主链路的探究,hook
的实现无非要做两件事:
- 绑定到当前的函数组件对应的
fiber
,这样才能嵌入render
和commit
主链路。 - 开启渲染调度。
我们再来看看上面的示例,好像也没有看到明显的绑定呀。setHookState
开启渲染调度,我们可以大概联想到,因为在class
组件中this.setState
调用,也是合并当前组件的状态,然后调用scheduleUpdateOnFiber
,开启调度。
js
const [hookState, setHookState] = useState(0)
仅凭这行代码我们也没有看出来,它如何实现绑定的。接下来我们还是从源码调试入手,看看它是如何实现的。
hook实现
第一部分:fiber与hook关联
我们可以把上面的示例修改一下:
js
import React, { useState } from 'react'
export default function hookState() {
const [hookState, setHookState] = useState(() => {
debugger
return 0
})
return (
<>
<div>
count: {hookState}
</div>
<button onClick={() => {
setHookState(preState => {
return preState + 1
})
}}>
点我
</button>
</>
)
}
这样重启项目的时候我们就能得到这样的调用栈了:

beginWork
是我们的老熟人了,我们关注它之后的哪些调用函数。
mountIndeterminateComponent
js
function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) {
resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);
var props = workInProgress.pendingProps;
var context;
var value;
var hasId;
// 这一步会收集 hooks(比如 useState、useEffect 等)
value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes);
workInProgress.flags |= PerformedWork;
// 省略
reconcileChildren(null, workInProgress, value, renderLanes);
return workInProgress.child;
}
}
mountIndeterminateComponent
做的事情比较多,因为这里我们只关注 hook
这部分的逻辑。在我们前面了解到的beginWork
阶段,主要将 react ele
翻译为与之对应的fiber
结构。我们重点关注 renderWithHooks
的实现。
renderWithHooks
js
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
renderLanes = nextRenderLanes;
currentlyRenderingFiber$1 = workInProgress;
//Tip:重要 - 当前正在构造的 fiber, 等同于 workInProgress
// 清除当前fiber的遗留状态
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// 执行function函数, 其中进行分析Hooks的使用
var children = Component(props, secondArg);
// Check if there was a render phase update
ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;
// 省略...
// 重置全局变量,并返回
// 执行function之后, 还原被修改的全局变量, 不影响下一次调用
var didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;
renderLanes = NoLanes;
currentlyRenderingFiber$1 = null;
currentHook = null;
workInProgressHook = null;
// currentHook 与 workInProgressHook:
// 分别指向 current.memoizedState 和 workInProgress.memoizedState
didScheduleRenderPhaseUpdate = false;
// This is reset by checkDidRenderIdHook
// localIdCounter = 0;
return children;
}
renderWithHooks
大概做了三件事情:
- 维护全局变量,比如
currentlyRenderingFiber$1
,这个也是后续hook
收集的中间桥梁。 var children = Component(props, secondArg);
,执行function函数, 其中进行分析Hooks的使用。- 重置全局变量,比如
currentHook
,workInProgressHook
等等。
接下来我们看看在执行这行var children = Component(props, secondArg)
代码时,发了什么。
useState
Component
对应的就是我们的函数组件,当它调用时,就会执行useState()
。
js
// react
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
// react-dom
useState: function (initialState) {
currentHookNameInDev = 'useState';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
useState
是react
暴露的api
,但是内部却要关联到react-dom
的运行时,resolveDispatcher()
就是拿二者到联系纽带。
mountState
那么重点来了,mountState
是fiber
和hook
主要的关联实现。
js
// useState 的返回
function mountState(initialState) {
// 先处理hook,获取当前的hook,在 mount 阶段
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// hook queue,每个 useState 对应一个 hook 对象。
// 产生的 update 保存在 useState 对应的 hook.queue中
var queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
hook.queue = queue;
var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
诶呀,但是这里好像没有二者关联的部分,函数的后半部分,主要构造queue
队列,然后将初始的state
和更新的dispatch
以数组的结构返回。函数的前端半部分,我们看看。
mountWorkInProgressHook
js
// 构造hook
function mountWorkInProgressHook() {
// hook定义
var hook = {
memoizedState: null, // 当前状态: 保持在内存中的局部状态.
baseState: null, // 基状态: hook.baseQueue中所有update对象合并之后的状态.
baseQueue: null, // 基队列: 存储update对象的环形链表, 只包括高于本次渲染优先级的update对象
queue: null, // 更新队列: 存储update对象的环形链表, 包括所有优先级的update对象.
next: null // next指针: next指针, 指向链表中的下一个hook.
};
// 将 hook 插入 fiber.memoizedState 链表末尾
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
// Tip:
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
currentlyRenderingFiber$1.memoizedState
对应的就是fiber.memoizedState
,这样我们就把hook
与fiber
关联上了。这里还有workInProgressHook
,主要用于hook state
持久化。
第二部分:hook触发调度
那么当我们调用 setHookState
是如何触发更新的呢,在前部分mountState
中,我们关注到这行代码var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue)
。
dispatchSetState
js
function dispatchSetState(fiber, queue, action) {
var lane = requestUpdateLane(fiber);
// hook update 对象
var update = {
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null
};
// 什么条件下 ? render阶段触发的更新?
// 为什么会render阶段触发更新?=> bad code
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
var alternate = fiber.alternate;
// TODO:
var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
var eventTime = requestEventTime();
// 调度任务更新
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane);
}
dispatchSetState
做了这么几件事情: 1.创建一个 update
,包含 lane
、action
(即 setState
的参数)、hasEagerState
。 2.判断是否在 render
阶段触发更新,如果是的话,加入到hook.queue
中;如果不是的话,加入到ConcurrentQueue
中,然后调用scheduleUpdateOnFiber
,启动调度更新。
总结
state hook
主要围绕如何关联当前fiber
,以及后续触发更新,将状态融入到函数组件当中。- 对于何时用全局上下文参数以及函数上下文参数有了更多的理解。