源码结构
在渲染函数组件的时候会调用renderWithHooks方法:
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// 1
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
...
// 2
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
...
// 3
// 根据当前workInprogress树上的Fiber在current树上是否有对应的Fiber节点
// 来判断是创建hook,还是更新hook
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 4
// 调用函数组件
let children = Component(props, secondArg);
...
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
// 5
// 重置hook链表指针
currentHook = null;
workInProgressHook = null;
...
return children;
}
可以看到这个函数中:
- 将
nextRenderLanes(当前需要渲染的优先级集合)赋值给了renderLanes,将workInProgress(函数组件对应的Fiber对象)赋值给了currentlyRenderingFiber,renderLanes和currentlyRenderingFiber是全局变量,在后面调用hook函数时使用。 - 将
workInProgress的三个属性重置了:memoizedState,updateQueue,NoLanes - 根据当前节点在current树上是否有相对应的节点判断是创建还是更新,以此来给
ReactCurrentDispatcher.current赋值:
const HooksDispatcherOnMount: Dispatcher = {
...
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
...
};
const HooksDispatcherOnUpdate: Dispatcher = {
...
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
...
};
创建阶段赋值为HooksDispatcherOnMount,更新阶段赋值为HooksDispatcherOnUpdate,可以看到这两个对象中保存的都是hook函数,key都是一样的,只是在不同阶段所调用的hook函数是不一样的。
- 调用函数组件,此时会调用hook函数,返回React element对象
- 重置hook链表指针:
currentHook,workInProgressHook,关于这两个指针十分重要,下面会详细分析。
组件挂载
下面我们则详细分析第4部分的代码:调用函数组件。
例如,我们现在有这样一个函数组件:
export default function Demo () {
const [num, setNum] = useState('value1');
const [num1, setNum1] = useState('value2');
const onTapBtn = () => {
setNum(num + 1);
}
return (
<button onClick={onTapBtn} >Demo:{num}</button>
);
}
调用函数组件后,首先会执行useState('value1'),那么我们来看下useState的源码:
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
...
return ((dispatcher: any): Dispatcher);
}
可以看到useState中首先会获取dispatcher,也就是我们上面所提到的第3部分的代码,将保存了hook函数的对象赋值给了ReactCurrentDispatcher.current,在useState函数中调用了resolveDispatcher方法获取了ReactCurrentDispatcher.current,随后调用了ReactCurrentDispatcher.current中的useState方法并传入了初始化的state。
在dispatcher.useState的函数内部,又调用了mountState函数:
function mountState(initialState) {
const hook = mountWorkInProgressHook();
...
}
在mountState中调用了mountWorkInProgressHook函数,我们来看一下mountWorkInProgressHook函数做了什么:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
首先创建了一个hook对象,然后判断workInProgressHook是否为null,因为每次调用完函数组件,都会把workInProgressHook置为null,也就是我们刚开始分析的源码结构的第5部分。此时又是第一次调用hook函数,workInProgressHook肯定为null,所以会将新建的hook对象赋值给workInProgressHook,然后将workInProgressHook挂载到currentlyRenderingFiber(源码结构的第1部分:函数组件对应的Fiber对象)的memoizedState属性上。
最后返回workInProgressHook,记住此时workInProgressHook指向的是useState('value1')所创建的hook对象。
mountWorkInProgressHook函数执行完成后,继续向下执行:
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null, // 需要更新的state
interleaved: null,
lanes: NoLanes, // 更新的lane优先级
dispatch: null, // 发起更新state的函数
lastRenderedReducer: basicStateReducer, // 上一次计算state的函数
lastRenderedState: (initialState: any), // 上一次更新的state
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
先判断了initialState,也就是我们使用useState时所传入的参数,如果是函数类型的,则会调用,获取到返回值。然后将值赋值给了hook对象上的baseState和memoizedState属性。
然后创建了一个队列对象,接着将dispatchAction函数通过bind方法传入函数组件对应的fiber对象和队列对象,挂载到队列对象的dispatch属性上。
最后返回一个集合,第一个元素是state的值,第二个元素是更新state函数(dispatchAction)。
此时useState('value1')调用完毕。
接着调用useState('value2'),流程也是与useState('value1')是一样的,不同点在于workInProgressHook的指向,我们来看一下:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
首先也是会为useState('value2')创建一个hook对象,继续执行,此时workInProgressHook指向的是useState('value1')创建的hook对象,则会将useState('value2')创建的对象挂载到useState('value1')创建的hook对象下的next属性上,形成一个hook对象链表。
在挂载阶段hook创建到此也完成了。
组件更新
还是以之前的例子来讲,现在我们点击Button,调用了setNum去更新state,上面讲过更新state的函数其实就是dispatchAction,我们来看一下源码:
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
...
const eventTime = requestEventTime(); // 获取更新触发时的时间
const lane = requestUpdateLane(fiber); // 获取更新优先级
// 创建更新对象
const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// 获取workInProgress树上对应的fiber
const alternate = fiber.alternate;
// currentlyRenderingFiber:是在render阶段调用函数组件前将函数组件对应的workInprogress的fiber赋值给了它,在调用函数组件后重置为null
// 如果fiber === currentlyRenderingFiber ,则说明该函数组件正在执行更新任务,将需要更新的数据添加到更新链表中即可,不用产生更新任务进行调度
if (
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
...
} else {
if (isInterleavedUpdate(fiber, lane)) {
...
} else {
// 将创建对象连接形成一个环形链表
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = 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);
// 获取最新的state
const eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
// 判断上一次更新的值与本次更新的值是否相同,不同则执行更新,相同直接返回不会执行更新
if (is(eagerState, currentState)) {
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
if (__DEV__) {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
}
}
...
}
可以看到dispatchAction中主要是创建了一个更新对象,将更新对象挂载到queue的pending属性上,那有的同学可能有疑惑了,fiber和queue哪来的?还记的我们分析函数组件挂载的时候,最后将dispatchAction函数通过bind方法传入函数组件对应的fiber对象和队列对象:
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
...
const queue = (hook.queue = {
pending: null, // 需要更新的state
interleaved: null,
lanes: NoLanes, // 更新的lane优先级
dispatch: null, // 发起更新state的函数
lastRenderedReducer: basicStateReducer, // 上一次计算state的函数
lastRenderedState: (initialState: any), // 上一次更新的state
});
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
所以我们在更新state时,能够获取到函数组件对应的fiber对象和queue对象。
将更新对象挂载到queue的pending属性上前,会将更新对象形成一个环形链表:
update1 update1.next -> update2 update2.next -> update1
之后调用lastRenderedReducer函数获取最新的state值:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
lastRenderedReducer实际上就是basicStateReducer函数,在它内部对action也就是我们调用更新函数传入的值,判断它是不是函数,是函数则会传入上一次的值并调用该函数获取到最新的值,如果不是函数,则直接返回新传入的值。
然后将最新的state,赋值给更新对象的eagerState属性上。
接着对eagerState(本次更新的state)和currentState(上一次更新的state)使用Object.is进行对比,如果相等,则会return不会进行更新,反之,则会进行更新。
当进行更新时,则又会调用renderWidthHooks函数。
给ReactCurrentDispatcher.current赋值为HooksDispatcherOnUpdate。
然后调用函数组件,这是函数组件内部的hook函数又会执行一遍,我们来看看此时又是如何执行的。
首先还是会调用useState('value1'),和挂载一样,但是不同的是,不再调用mountState函数,而是调用updateState函数:
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
而updateState内部调用了updateReducer函数:
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
...
}
在updateReducer内部首先调用了updateWorkInProgressHook去获取对应的hook对象:
function updateWorkInProgressHook(): Hook {
// currentlyRenderingFiber是workInprogress树上的Fiber
// currentlyRenderingFiber.alternate则是对应在current树上的Fiber
// nextCurrentHook的值则是current树上的Fiber的hook链表
let nextCurrentHook: null | Hook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
// nextWorkInProgressHook的值则是workInProgress树上的Fiber的hook链表
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
// 将currentHook作为current树上的Fiber的hook链表的指针
// 将currentHook作为workInProgressHook作为对应workInProgress树上的Fiber的hook链表的指针
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
我们知道每次调用完函数组件后都会将currentHook和workInProgressHook都重置为null。
- 会将
nextCurrentHook赋值为current树上函数组件对应fiber上的memoizedState(hook对象链接)属性,也可以理解为,将nextCurrentHook指向为current树上函数组件的hook链表的第一个hook对象。 nextWorkInProgressHook赋值为workInProgress树上函数组件对应fiber上的memoizedState(hook对象链接)属性,但是在每次调用函数组件前,都会将函数组件在workInProgress树上对应fiber的memoizedState置为null,所以nextWorkInProgressHook第一次总是为null。nextWorkInProgressHook为null,则会根据currentHook创建一个新的hook对象,然后将新的对象赋值给workInProgressHook,并挂载在函数组件在workInProgress树上对应fiber的memoizedState属性上。- 将
nextCurrentHook赋值给currentHook - 返回
workInProgressHook
通过这个函数我们发现有两个指针:currentHook,workInProgressHook:
currentHook:指向的是current树上函数组件对应的hook链表上的hook对象workInProgressHook:指向的是workInProgress树上函数组件对应的hook链表上的hook对象
并且它们两个指针指向的hook对象是一一对应的:此时currentHook指向的是current树上函数组件对应的hook链表上的第一个对象useState('value1'),workInProgressHook指向的是根据currentHook指向的hook对象创建出来的新的hook对象,也是useState('value1')。
updateWorkInProgressHook调用完成后,通过currentHook可以拿到上一次的state值,通过遍历更新对象形成的环形链表计算出最终的state。
最后也会返回一个集合,第一个元素是最新state值,第二个元素是更新函数。
为什么需要将hook函数放在函数组件的顶层
举个例子:
const [num, setNum] = useState('value1'); // useState1
const [num2, setNum2] = useState('value2'); // useState2
现在创建完成后的hook链表是这样:
useState1 -> useState2
我们现在将useState1放在if里面,让它在更新阶段不执行:
let isMount = false;
// 只在创建阶段执行,更新阶段不执行
if(!isMount) {
const [num, setNum] = useState('value1'); // useState1
isMount = true;
}
const [num2, setNum2] = useState('value2'); // useState2
然后我们更新useState2的值:
setNum2('update');
在更新阶段,会根据current上的hook链表创建workInprogress的hook链表,虽然useState1不会执行,但是在执行useState2的时候,会去获取current上的hook链表的第一个对象useState1-hook,以此创建useState2在workInprogress的hook链表上的hook对象,所以此时useState2的hook对象是useState1-hook,但是更新操作是在useState2上发起的,useState1并不会更新,所以会获取useState1上一次更新的值:value1,然后返回给useState2,最终useState2得到更新的值则为:value1。
hook链表:
curent树上的hook链表: useState1 hook -> useState2 hook
workInprogress树上的hook链表: useState1 hook
之所以结果会是这样,是因为在创建时,会根据hook函数书写的顺序创建hook链表,在更新时,使用指针从hook链表中依次取出对应的hook对象进行执行,将hook函数放入了if中相当于将后面的hook函数的执行提前,与hook链表中所对应的hook对象的位置就错乱了,则会造成无法预期的结果。
总结
- 在挂载函数组件时,会根据hook函数书写的位置创建一个hooks链表,挂载在函数组件对应的fiber的
memoizedState属性上。 - 在更新函数时,使用
currentHook指针依次取出函数组件对应在current树上fiber的hooks链表中的hook对象,然后根据currentHook来创建或复用,以此创建workInprogress树上函数组件的hooks链表,并且会使用workInprogressHook指针取出hook对象。workInprogressHook与currentHook是一一对应的。 - 使用两个指针:
currentHook,workInprogressHook的目的是,使用currentHook可以获取上一次hook函数更新的状态,协助本次更新,比如:- 使用了
useEffect,上一次传入的依赖数组为:[a=1],本次更新依赖数组为[a=2], 两次对比不同则需要执行回调函数。 - 使用了
useState,本次调用更新函数传入一个函数,我们知道回调函数会将上一次的值作为参数传入,那么这个参数从哪里来的?是的,从curent树上的hook链表中hook函数对应的hook对象上取出来的。
- 使用了