为什么需要 hook
React 文档中也有说明了 Hooks 的提出主要是为了解决什么问题的:
组件之间复用状态逻辑很难。
就以前 React 为了将一个组件的逻辑抽离复用,不和渲染代码混用在一个 class 的做法,比较推介的是用高阶组件,将状态逻辑抽离出来,通过不同的样式组件传入带有状态逻辑的高阶组件中,增强样式组件的功能,从而达到复用逻辑的功能。再早期就会使用 mixin 去实现。
缺点:增加嵌套层级,代码会更加难以理解。
复杂组件变得难以理解
在使用 class 组件的时候,我们少不了在生命周期函数中添加一些操作,例如调用一些函数,或者去发起请求,在销毁组件的时候为了防止内存溢出,我们可能还需要对一些事件移除。那么这个时候我们就需要在componentDidMount,componentDidUpdate 中可能会调用相同的函数获取数据,componentWillUnmount 中移除事件等;这些因为和组件有很强的耦合性,也很难通过高阶组件的方式抽离出来,而 Hook 将组件中关联的部分拆分成更小的函数,每个函数功能更加单一。
难以理解的 class
Hook 就是一个以纯函数的方式存在的class组件;以前我们使用纯函数组件时都有一个标准,就是这个组件并不具备自身的生命周期使用,以及自己独立的state。只是单纯的返回一个组件。或者是根据传入的 props 组装组件。但随着 Hook 的发布,React 团队是想将 React 更加偏向函数式编程的方式编写组件,让本来存函数组件变得可以使用 class 组件的一些特性。
hook 调用入口
在 hook 源码中 hook 存在于 Dispatcher 中,Dispatcher 就是一个对象,不同 hook 调用的函数不一样;在 renderWithHooks 函数中,在 FunctionComponent render 前, 会根据 FunctionComponent 对应 fiber 的以下条件区分 mount 与 update。
current === null || current.memoizedState === null
并将不同情况对应的dispatcher赋值给全局变量ReactCurrentDispatcher的current属性。
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
// mount 时的 Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
...
}
// mount 时的 Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
...
};
在 FunctionComponent render 时,会从 ReactCurrentDispatcher.current(即当前 dispatcher)中寻找需要的hook。换言之,不同的调用栈上下文为 ReactCurrentDispatcher.current 赋值不同的 dispatcher,则FunctionComponent render 时调用的 hook 也是不同的函数。
Hook的数据结构
通过 mountWorkInProgressHook 函数进行创建 hook 的数据结构:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null, // 对于不同 hook,有不同的值
baseState: null, // 初始 state
baseQueue: null, // 初始 queue 队列
queue: null, // 需要更新的 update
next: null, // 下一个 hook
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
这里除了 memoizedState 字段外,其他字段与之前介绍的 updateQueue 的数据结构字段意义类似。
hook 链表结构
上面创建的 hook 结构中,通过 next 指针,实现了一个无环的单向链表。产生的 hook 对象依次排列,形成链表存储到函数组件 fiber.memoizedState 上。
{
baseQueue: null,
baseState: 'hook1',
memoizedState: null,
queue: null,
next: {
baseQueue: null,
baseState: null,
memoizedState: 'hook2',
next: null
queue: null
}
}
memoizedState
下面来看下不同的 hook 中 memoizedState 对应的值:
- useState:例如
const [state, updateState] = useState(initialState)
;memoizedState 等于state 的值。 - useReducer:例如
const [state, dispatch] = useReducer(reducer, {});
;memoizedState 等于state 的值。 - useEffect:在 mountEffect 时会调用 pushEffect 创建 effect 链表,memoizedState 就等于 effect 链表,effect 链表也会挂载到 fiber.updateQueue 上,每个 effect 上存在 useEffect 的第一个参数回调和第二个参数依赖数组,例如,useEffect(callback, [dep]),effect 就是 {create:callback, dep:dep,…}。
- useRef:例如 useRef(0),memoizedState 就等于 {current: 0}。
- useMemo:例如 useMemo(callback, [dep]),memoizedState 等于 [callback(), dep]。
- useCallback:例如 useCallback(callback, [dep]),memoizedState 等于 [callback, dep]。这里需要注意的是 useCallback 保存 callback 函数,useMemo 保存 callback 的执行结果。
注意:
hook 与 FunctionComponent fiber 都存在 memoizedState 属性。
- fiber.memoizedState:FunctionComponent 对应 fiber 保存的 Hooks 链表。
- hook.memoizedState:Hooks 链表中保存的单一 hook 对应的数据。
workInProgressHook 指针
在产生 hook 对象过程中,有一个十分重要的指针:workInProgressHook,它通过记录当前生成(更新)的 hook 对象,来指向在组件中当前调用到哪个 hook 函数了。每调用一次 hook 函数,就将这个指针的指向移到该 hook 函数产生的 hook 对象上。例如:
const App = () => {
const [ num, setNum ] = useState(0)
useEffect(() => { console.log('component update') })
const [ state, setState ] = useState(false)
return <div>app</div>
}
上面的代码中,App 组件内一共调用了三个 hooks 函数,分别是useState、useEffect、useState。那么构建 hook 链表的过程, workInProgressHook 的指向如下变化。
调用 useState(0):
fiber.memoizedState: hookNum
^
workInProgressHook
调用 useEffect:
fiber.memoizedState: hookNum -> hookEffect
^
workInProgressHook
调用 useState(false):
fiber.memoizedState: hookNum -> hookEffect -> hookState
^
workInProgressHook
极简 hook 实现
下面是实现一个简单的但是整个工作流程完整的 useState:
// 是否为首次渲染
let isMount = true
// 指向当前正在指向的 hook
let workInProgressHook = null
function run() {
// 模拟 render 阶段
// 更新前将 workInProgressHook 重置为 fiber 保存的第一个 Hook
// 通过 workInProgressHook 变量指向当前正在工作的 hook。
workInProgressHook = fiber.memolizedState
// 触发 function component 对应函数
// 触发组件 render
const app = fiber.stateNode()
// 组件首次 render 为 mount,以后再触发的更新为 update
isMount = false
return app
}
// App 组件对应的 fiber 对象
const fiber = {
// 保存该 FunctionComponent 对应的 Hooks 链表
memolizedState: null,
// 指向 App 函数
stateNode: App
}
// 创建 update,并将这些 update 形成一个环状链表保存在 hook.queue.pending 中
function dispatchAction(queue, action) {
// 创建 update 数据结构
const update = {
// 更新执行的函数
action,
// 链接其他 update,形成环状链表
next: null
}
// 表示之前不存在 update
if (queue.pending === null) {
// 第一个 update 会跟自己形成一个环状链表
update.next = update
} else {
// 3 -> 0 -> 1 -> 3
// 新增 4 update
// 4 -> 0 -> 1 -> 3 -> 4
// queue.pending.next 保存第一个 update
// 4 -> 0
update.next = queue.pending.next
queue.pending.next = update
}
// 指向最后一个 update
queue.pending = update
run()
}
function useState(initialStste) {
let hook
if (isMount) {
// 创建 hook 数据结构
hook = {
queue: {
pending: null
},
// 保存 hook 对应的 state 属性
memolizedState: initialStste,
// 指向下一个 hook
next: null
}
if (!fiber.memolizedState) {
fiber.memolizedState = hook
} else {
workInProgressHook.next = hook
}
workInProgressHook = hook
} else {
hook = workInProgressHook
workInProgressHook = workInProgressHook.next
}
// 无优先级概念,baseState 就是 memolizedState
let baseState = hook.memolizedState
// hook 有需要计算的 update
if(hook.queue.pending) {
// pending 值也为环状链表;hook.queue.pending 保存的是最后一个 update
let firstUpdate = hook.queue.pending.next
do {
// action 为调用 updateNum 中传入的参数,可以为一个 state 值,
// 也可以为一个 function;这里做一个简化只考虑传入 function 的情况
const action = firstUpdate.action
baseState = action(baseState)
firstUpdate = firstUpdate.next
// 遍历这条链表直到不等于一个 update 为止
} while(firstUpdate !== hook.queue.pending.next)
// update 计算完成
hook.queue.pending = null
}
hook.memolizedState = baseState
return [baseState, dispatchAction.bind(null, hook.queue)]
}
function App() {
const [num, updateNum] = useState(0)
const [status, triggerStatus] = useState(false)
console.log('isMount', isMount)
console.log('num', num)
console.log('status', status)
return {
onClick() {
updateNum(num => num + 1)
},
trigger() {
triggerStatus(status => !status)
}
}
}
window.app = run()
上面代码模拟了 useState 的整个更新流程;在 react 中大体也是这样的一个更新流程,相比于 react hook,上面代码有以下不足:
- React Hooks 没有使用 isMount 变量,而是在不同时机使用不同的 dispatcher。换言之,mount 时的 useState 与 update 时的 useState 不是同一个函数。
- React Hooks 有中途跳过更新的优化手段。
- React Hooks 有 batchedUpdates,当在 click 中触发三次 updateNum,上面代码会触发三次更新,而 React 只会触发一次。
- React Hooks 的 update 有优先级概念,可以跳过不高优先的 update。
useState 与 useReducer
这里之所以将 useState 与 useReducer 放在一起是因为 useState 只是预置了 reducer 的 useReducer。下面通过声明阶段以及触发状态改变执行阶段两个方面来看看这两个 hook 的执行代码;
声明阶段
当 FunctionComponent 进入 render 阶段的 beginWork 时,在 beginWork 中根据不同的 tag,执行不同的更新逻辑。function Component 执行 mountIndeterminateComponent 方法;然后在该方法中会执行 function Component;通过 ReactCurrentDispatcher.current 获取对应的 hook 进行执行;
对于这两个Hook,他们的源码如下:
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function useReducer(reducer, initialArg, init) {
var dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArg, init);
}
然后根据 mount 与 update 两个场景下调用不同 dispatcher 对应的 hook 方法来进行初始化;
mount
mount 阶段 useState 调用 mountState,useReducer 调用 mountReducer:
useReducer: function (reducer, initialArg, init) {
currentHookNameInDev = 'useReducer';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountReducer(reducer, initialArg, init);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
useState: function (initialState) {
currentHookNameInDev = 'useState';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
接下来看看 mountState 以及 mountReducer 方法,这两个方法唯一区别就是它们创建的 queue 中lastRenderedReducer 不一样,mount 有初始值 basicStateReducer,所以说 useState 就是内置了 reducer 参数的 useReducer。
function mountState(initialState) {
// 创建并返回当前的 hook
var hook = mountWorkInProgressHook();
// 赋值初始 state
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 创建 queue
var queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
hook.queue = queue;
// 创建 dispatch
var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
function mountReducer(reducer, initialArg, init) {
// 创建并返回当前的 hook
var hook = mountWorkInProgressHook();
var initialState;
// 赋值初始 state
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = initialArg;
}
hook.memoizedState = hook.baseState = initialState;
// 创建 queue
var queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: initialState
};
hook.queue = queue;
// 创建 dispatch
var dispatch = queue.dispatch = dispatchReducerAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
执行阶段
首先会执行上面声明阶段返回的 dispatchSetState 、dispatchReducerAction 方法;
整个过程可以概括为:创建 update,将 update 加入 queue.pending 中,并开启调度。
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
// 是否为 render 阶段触发的更新
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
enqueueUpdate(fiber, queue, update, lane);
const alternate = fiber.alternate;
// fiber.lanes 保存 fiber 上存在的 update 的优先级。
// fiber.lanes === NoLanes 意味着 fiber 上不存在 update。
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
}
}
}
const eventTime = requestEventTime();
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane, action);
}
function dispatchReducerAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
enqueueUpdate(fiber, queue, update, lane);
const eventTime = requestEventTime();
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane, action);
}
上面代码中的 if 判断:
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
)
fiber.lanes === NoLanes
意味着 fiber 上不存在 update;我们已经知道,通过 update 计算 state 发生在声明阶段,这是因为该 hook 上可能存在多个不同优先级的 update,最终 state 的值由多个 update 共同决定。
但是当 fiber 上不存在 update,则调用阶段创建的 update 为该 hook 上第一个 update,在声明阶段计算 state 时也只依赖于该 update,完全不需要进入声明阶段再计算 state。
这样做的好处是:如果计算出的 state 与该 hook 之前保存的 state 一致,那么完全不需要开启一次调度。即使计算出的 state 与该 hook 之前保存的 state 不一致,在声明阶段也可以直接使用调用阶段已经计算出的 state。
调用栈
声明阶段
在 beginWork 中根据不同的 tag,执行不同的更新逻辑。function Component 执行 mountIndeterminateComponent 方法;然后在该方法中会执行 function Component;通过 ReactCurrentDispatcher.current 获取对应的 hook 进行执行;