(一)useCallback
总的思路:根据输入的deps,也就是一个数组其内部的内容是否有变化,决定是返回存储的老方法,还是返回新的方法并记录
挂载组件阶段
在这个mount阶段中,主要完成hook实例的创建,以及缓存当前的callback及其依赖列表到hook的memorizedState中,返回回调函数。
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 创建一个hook实例
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 缓存当前的callback及其依赖列表
hook.memoizedState = [callback, nextDeps];
return callback;
}
组件更新阶段
在update阶段进行依赖比较,如果新依赖与旧依赖一致(其比较方式采用的是Object.is比较方法),返回原callback。如果不一致,返回新的callback,并缓存新callback以及新依赖列表。
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 从hook的memorizedState中获取上次保存的值[callback, deps]
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// 比较新旧依赖列表
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果相等,返回memorized的callback
return prevState[0];
}
}
}
// 如果deps发生变化,更新hook的memorizedState,并返回最新的callback
hook.memoizedState = [callback, nextDeps];
return callback;
}
(二)useMemo
挂载组件阶段
在mount阶段,执行创建函数获得返回值,
保存到hook的memorizedState中。
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
可以看到,与mountCallback相比这两个唯一的区别是
● mountMemo会将回调函数(nextCreate)的执行结果作为value保存
● mountCallback会将回调函数作为value保存
组件更新阶段
在组件更新阶段,获取新的deps,
从memorizedState中获得上次保存的值,比较新deps和旧deps是否相等,如果两者相等,返回旧的创建函数的返回值。
如果deps发生改变,hook中保存新的返回值和deps,并返回新的创建函数的返回值。
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// 判断update前后value是否变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 未变化
return prevState[0];
}
}
}
// 变化,重新计算value
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
可见,对于update,这useCallback和useMemo两个hook的唯一区别也是是回调函数本身还是回调函数的执行结果作为value。
(三)useRef
挂载组件阶段
在这个阶段,useRef和其他Hook一样创建一个Hook对象,然后创建一个{current: initialValue}的值,缓存到Hook的memoizedState属性,并返回该值
function mountRef<T>(initialValue: T): {|current: T|} {
// 获取当前useRef hook
const hook = mountWorkInProgressHook();
// 创建ref
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
组件更新阶段
这个阶段直接从Hook实例中返回之前缓存的值。
function updateRef<T>(initialValue: T): {|current: T|} {
// 获取当前useRef hook
const hook = updateWorkInProgressHook();
// 返回保存的数据
return hook.memoizedState;
}
(四) useState
根据下面的源码,可见useState不过就是个语法糖,本质其实就是useReducer,或者说,useState只是预置了reducer的useReducer。
当调用setState方法(对应的就是useReducer返回的dispatch函数)时,触发basicStateReducer函数,第一个参数接收oldState,第二个参数是setState被调用时传入的参数newState,但是因为newState不是函数类型,直接返回newState,完成state的更新。
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return useReducer(
basicStateReducer,
// useReducer has a special case to support lazy useState initializers
(initialState: any),
);
}
(五)useReducer
useReducer是比较重要的hook,笔者详细解说一下,注意下面笔者展示的代码都经过简化,去掉了静态类型和fiber有关的逻辑,保留最本质的Hook有关逻辑。
对于useReducer Hook,考虑如下例子:
export default function App() {
const reducer = (state,action)=> {
if(action === 'add'){
return state + 1;
}
return state;
}
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<h1 className="title">{count}</h1>
<button className="btn is-primary"
onClick={()=> dispatch('add')}
>Increment</button>
</div>
)
}
我们可以将它的实现分为三个部分:
1.通过一些途径产生更新,更新会造成组件render。
(1)调用ReactDOM.render会产生mount的更新,更新内容为useReducer的initialValue(即0)。
(2)点击button标签触发dispatch会产生一次update的更新,更新内容为reducer。
2. 组件render时useReducer返回的count为更新后的结果
更新是什么?
首先我们要明确更新是什么。React是如何在每次重新渲染之后都能返回最新的状态? Class Component因为自身的特点可以将私有状态持久化的挂载到类实例上,每时每刻保存的都是最新的值。而 Function Component 由于本质就是一个函数,并且每次渲染都会重新执行。所以React必定拥有某种机制去记住每一次的dispatch更新操作,并最终得出最新的count值返回。
实际上,更新就是如下数据结构:
const update = {
// 更新执行的函数
action,
// 与同一个Hook的其他更新形成链表
next: null
}
对于App来说,点击button标签产生的update的action为reducer。
update数据结构
如果我们点击button标签三次,会执行三次dispatch,产生三次update,那么这些dispatch是如何组合到一起的呢?
答案是他们会形成环状单向queue链表。queue链表会存放每一次的更新,以便后面的update阶段可以返回最新的状态。
每次我们调用dispatch的时候,实际上我们调用的是dispatchAction.bind(null, hook.queue),当调用dispatchAction方法的时候,会形成一个新的update对象,添加到queue环状单向链表上。
我们先来了解下dispatchAction这个函数:
function dispatchAction(fiber,queue,action,) {
const update = {
action,
next: null,
};
// 将update对象添加到循环链表中
const last = queue.last;
if (last === null) {
// 链表为空,将当前更新作为第一个,并保持循环
update.next = update;
} else {
const first = last.next;
if (first !== null) {
// 在最新的update对象后面插入新的update对象
update.next = first;
}
last.next = update;
}
// 将表头保持在最新的update对象上
queue.last = update;
// 进行调度工作
scheduleWork();
}
下面我们举例解释一下dispatchAction:
当产生第一个update(我们叫他u0),此时queue.last === null。
update.next = update;即u0.next = u0,他会和自己首尾相连形成单向环状链表。
然后queue.pending = update;即queue.last = u0
queue.last = u0 ---> u0
^ |
| |
---------
当产生第二个update(我们叫他u1),update.next = queue.last.next;,此时queue.last.next === u0, 即u1.next = u0。
queue.last.next = update;,即u0.next = u1。
然后queue.last = update;即queue.last = u1
queue.last = u1 ---> u0
^ |
| |
---------
照着这个例子模拟插入多个update的情况,会发现queue.last始终指向最后一个插入的update。
这样做的好处是,当我们要遍历update时,queue.last.next指向第一个插入的update。
React 如何管理区分Hooks
如果App组件中有多个useReducer,那么React 如何管理区分这些Hooks呢?
这里我们可以从 mountState 里的 mountWorkInProgressHook方法和Hook的类型定义中找到答案
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
// 保存update的queue,即上文介绍的queue
queue: UpdateQueue<any, any> | null,
next: Hook | null, // 指向下一个Hook, 与下一个Hook连接形成单向无环链表
};
每个useReducer都对应一个hook对象,Hook是无环的单向链表。
这些Hooks节点是怎么利用链表数据结构串联在一起的呢?相关逻辑就在每个具体mount 阶段 Hooks函数调用的 mountWorkInProgressHook方法里:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// 当前workInProgressHook链表为空的话,
// 将当前Hook作为第一个Hook
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// 否则将当前Hook添加到Hook链表的末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
如上,workInProgressHook我们可以理解为是一个按Hooks的执行顺序依次将Hook节点添加到链表中的辅助指针。
状态如何保存
好的,现在我们已经了解了React 通过链表来管理 Hooks,同时也是通过一个循环链表来存放每一次的更新操作,得以在每次组件更新的时候可以计算出最新的状态返回给我们。那么我们这个Hooks链表又存放在那里呢?理所当然的我们需要将它存放到一个跟当前组件相对于的地方。这个与组件一一对应的地方就是我们的FiberNode,组件构建的Hooks链表会挂载到FiberNode节点的memoizedState上面去。
mounted阶段源码:
mount 阶段 Hooks函数会调用mountReducer方法,它首先通过调用mountWorkInProgressHook,创建每个useReducer对应的hook对象,并且利用workInProgressHook指针,将存储在currentlyRenderingFiber.memoizedState上的hook对象们都串联起来,最后返回
[hook.memoizedState, dispatch],也就是对应的调用useReducer返回的[count,distpatch]
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
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;
}
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, A> = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
调用dispatch阶段源码:
当我们点击按钮,触发dispatch函数的时候,如上面update数据结构小节中所讲,实际上我们调用的是dispatchAction.bind(null, hook.queue),当调用dispatchAction方法的时候,会形成一个新的update对象,添加到queue环状单向链表上,之后会调用 scheduleWork方法进行调度工作。过程可以概括为:
创建update,将update加入queue.last中,并开启调度。
update阶段源码:
// react-reconciler/src/ReactFiberHooks.js
// 去掉与fiber有关的逻辑
function updateReducer(reducer,initialArg,init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 拿到更新列表的表头
const last = queue.last;
// 获取最早的那个update对象
first = last !== null ? last.next : null;
if (first !== null) {
let newState;
let update = first;
do {
// 执行每一次更新,去更新状态
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
}
const dispatch = queue.dispatch;
// 返回最新的状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
组件render时,执行到useReducer的时候,会遍历update对象循环链表,执行每一次更新去计算出最新的状态来返回,以保证我们每次刷新组件都能拿到当前最新的状态。useReducer 的reducer是用户定义的reducer,所以会根据传入的action和每次循环得到的newState逐步计算出最新的状态。总结下来,这个阶段做的事情是
找到对应的hook,根据update计算该hook的新state并返回。
由这个阶段的源码,我们可以得知useReducer如何在每次渲染时,返回最新的值都原因如下:
● 每个Hook节点通过循环链表记住所有的更新操作
● 在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回
附件:手写简易useState
如下是摘自《React技术揭秘》一书的极简useState的实现代码,笔者认为对于理解useState和useReducer的关键源码有较大的帮助,遂附上供大家参考。
手写简易useState代码