1. 极简Hooks实现
function App() {
const [num, updateNum] = useState(0);
return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;
}
- 通过一些途径产生更新,更新会造成组件render--updateNum;
- 组件render时useState返回的num为更新后的结果;
其中步骤1的更新可以分为mount和update:
- 调用ReactDOM.render会产生mount的更新,更新内容为useState的initialValue(即0)。
- 点击p标签触发updateNum会产生一次update的更新,更新内容为num => num + 1。
1.1 更新是什么
通过一些途径产生更新,更新会造成组件render
const update = {
// 更新执行的函数
action,
// 与同一个Hook的其他更新形成链表
next: null
}
1.2 update的数据结构
加入有多个update,如何组合起来
// 之前
return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;
// 之后
return <p onClick={() => {
updateNum(num => num + 1);
updateNum(num => num + 1);
updateNum(num => num + 1);
}}>{num}</p>;
通过环形单向链表 调用updateNum实际上是dispatchAction.bind(null, hook.queue)
function dispatchAction(queue, action) {
// 创建update
const update = {
action,
next: null
}
// 环状单向链表操作
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
// 模拟React开始调度更新
schedule();
}
demo:
当产生第一个update(我们叫他u0),此时queue.pending === null。
update.next = update;即u0.next = u0,他会和自己首尾相连形成单向环状链表。
然后queue.pending = update;即queue.pending = u0
queue.pending = u0 ----->
^ |
| |
---------
当产生第二个update(我们叫他u1),update.next = queue.pending.next;,此时queue.pending.next === u0, 即u1.next = u0。
queue.pending.next = update;,即u0.next = u1。
然后queue.pending = update;即queue.pending = u1
queue.pending = u1 ---> u0
^ |
| |
---------
queue.pending始终指向最后一个插入的update
1.3 状态如何保存
更新产生的update对象会保存在queue中。不同于ClassComponent的实例可以存储数据,对于FunctionComponent,queue存储对应的fiber中。
// App组件对应的fiber对象
const fiber = {
// 保存该FunctionComponent对应的Hooks链表
memoizedState: null,
// 指向App函数
stateNode: App
};
1.4 Hooks数据结构
hook = {
// 保存update的queue,即上文介绍的queue
queue: {
pending: null
},
// 保存hook对应的state
memoizedState: initialState,
// 与下一个Hook连接形成单向无环链表
next: null
}
Q:update与hook的关系
A:每个useState对应一个hook对象。
调用const[num,updateNum] = useState(0);时updateNum(即上文介绍的dispatchAction)产生的update保存在useState对应的hook.queue中。
1.5 模拟react调度更新流程
1.实现通过操作产生更新,更新造成组件render
function dispatchAction(queue, action) {
// ...创建update
// ...环状单向链表操作
// 模拟React开始调度更新
schedule();
}
// 模拟调度
// 首次render时是mount
isMount = true;
function schedule() {
// 更新前将workInProgressHook重置为fiber保存的第一个Hook
workInProgressHook = fiber.memoizedState;
// 触发组件render
fiber.stateNode();
// 组件首次render为mount,以后再触发的更新为update
isMount = false;
}
// 每当遇到下一个useState,我们移动workInProgressHook的指针
workInProgressHook = workInProgressHook.next;
// 保证了每次组件render时useState的调用顺序及数量保持一致
// 可以通过workInProgressHook找到当前useState对应的hook对象。
1.6 计算state
2.组件render时,useState返回的值为更新后的结果,即一个完整的useState
function useState(initialState) {
// 当前useState使用的hook会被赋值该该变量
let hook;
if (isMount) {
hook = {
queue: {
pending: null
},
memoizedState: initialState,
next: null
}
// 将hook插入fiber.memoizedState链表末尾
if (!fiber.memoizedState) {
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook;
}
// 移动workInProgressHook指针
workInProgressHook = hook;
} else {
// update时找到对应hook
hook = workInProgressHook;
// 移动workInProgressHook指针
workInProgressHook = workInProgressHook.next;
}
let baseState = hook.memoizedState;
if (hook.queue.pending) {
// 获取update环状单向链表中第一个update
let firstUpdate = hook.queue.pending.next;
do {
// 执行update action
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;
// 最后一个update执行完后跳出循环
} while (firstUpdate !== hook.queue.pending.next)
// 清空queue.pending
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
2. Hooks数据结构
2.1 dispatcher
上文中,useState使用isMount区分mount和update
在真实的Hooks中,组件mount时的hook与update时的hook来源于不同的对象,这类对象在源码中被称为dispatcher
// mount时的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
// ...省略
};
// update时的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
// ...省略
};
mount时调用的hook和update时调用的hook其实是两个不同的函数。
在FunctionComponent render前,会根据FunctionComponent对应fiber的以下条件区分mount与update
current === null || current.memoizedState === null
并将不同情况对应的dispatcher赋值给全局变量ReactCurrentDispatcher的current属性
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
2.2 dispatch异常场景
useEffect(() => {
useState(0);
})
实际上,ReactCurrentDispatcher.current已经指向ContextOnlyDispatcher,所以调用useState实际会调用throwInvalidHookError,直接抛出异常
export const ContextOnlyDispatcher: Dispatcher = {
useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
// ...省略
2.3 Hook数据结构
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
除了memoizedState,其余与updateQueue一致
- useState:对于const [state, updateState] = useState(initialState),memoizedState保存state的值
- useReducer:对于const [state, dispatch] = useReducer(reducer, {});,memoizedState保存state的值
- useEffect:memoizedState保存包含useEffect回调函数、依赖项等的链表数据结构effect。effect链表同时会保存在fiber.updateQueue中
- useRef:对于useRef(1),memoizedState保存{current: 1}
- useMemo:对于useMemo(callback, [depA]),memoizedState保存[callback(), depA]
- useCallback:对于useCallback(callback, [depA]),memoizedState保存[callback, depA]。与useMemo的区别是,useCallback保存的是callback函数本身,而useMemo保存的是callback函数的执行结果
有些hook是没有memoizedState的,比如:
- useContext
2. useState和useReducer
useState和useReducer是Redux作者加入React后的一个核心贡献:将Redux的思想带入到React里
本质来说,useState只是预置了reducer的useReducer
2.1 概览
function App() {
const [state, dispatch] = useReducer(reducer, {a: 1});
const [num, updateNum] = useState(0);
return (
<div>
<button onClick={() => dispatch({type: 'a'})}>{state.a}</button>
<button onClick={() => updateNum(num => num + 1)}>{num}</button>
</div>
)
}
- 声明阶段即App调用时,会依次执行useReducer与useState方法
- 调用阶段即点击按钮后,dispatch或updateNum被调用时
2.2 声明阶段
当FunctionComponent进入render阶段的beginWork时,会调用renderWithHooks方法
该方法内部会执行FunctionComponent对应函数(即fiber.type)
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function useReducer(reducer, initialArg, init) {
var dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArg, init);
}
2.2.1 mount时
mount时,useReducer会调用mountReducer,useState会调用mountState
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 创建并返回当前的hook
const hook = mountWorkInProgressHook();
// ...赋值初始state
// 创建queue
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
// ...创建dispatch
return [hook.memoizedState, dispatch];
}
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 创建并返回当前的hook
const hook = mountWorkInProgressHook();
// ...赋值初始state
// 创建queue
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
// ...创建dispatch
return [hook.memoizedState, dispatch];
}
其中,mountWorkInProgressHook对应创建并返回对应的Hook,以上两个hooks的区别:
queue参数的lastRenderedReducer字段
const queue = (hook.queue = {
pending: null,
// 保存dispatchAction.bind()的值
dispatch: null,
// 上一次render时使用的reducer
lastRenderedReducer: reducer,
// 上一次render时的state
lastRenderedState: (initialState: any),
});
useReducer的lastRenderedReducer为传入的reducer参数。useState的lastRenderedReducer为basicStateReducer,basicStateReducer如下:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
2.2.2 update时
在update时,useReducer和useState调用的是同一个函数 updateReducer
// 找到对应的hook,根据update计算该hook的新state并返回
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 获取当前hook
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
// ...同update与updateQueue类似的更新逻辑
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
2.3 调用阶段
调用阶段会执行dispatchAction,此时该FunctionComponent对应的fiber以及hook.queue已经通过调用bind方法预先作为参数传入
// 创建update,将update加入queue.pending中,并开启调度。
function dispatchAction(fiber, queue, action) {
// ...创建update
var update = {
eventTime: eventTime,
lane: lane,
suspenseConfig: suspenseConfig,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
// ...将update加入queue.pending
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
// render阶段触发的更新
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
} else {
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
// ...fiber的updateQueue为空,优化路径
}
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}
4. useEffect
参考commit阶段时useEffect工作流
在flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取effectList。
4.1 flushPassiveEffectsImpl
flushPassiveEffects内部会设置优先级,并执行flushPassiveEffectsImpl flushPassiveEffectsImpl主要做三件事:
- 调用该useEffect在上一次render时的销毁函数;
- 调用该useEffect在本次render时的回调函数;
- 如果存在同步任务,不需要等待下次事件循环的宏任务,提前执行;
这里主要关注前两件事:
4.1.1 销毁函数的执行
useEffect的执行需要保证所有组件useEffect的销毁函数必须都执行完后才能执行任意一个组件的useEffect的回调函数。
这是因为多个组件间可能共用同一个ref。
如果不是按照“全部销毁”再“全部执行”的顺序,那么在某个组件useEffect的销毁函数中修改的ref.current可能影响另一个组件useEffect的回调函数中的同一个ref的current属性。
在useLayoutEffect中也有同样的问题,所以他们都遵循“全部销毁”再“全部执行”的顺序。
所以,会遍历并执行所有useEffect的销毁函数
// pendingPassiveHookEffectsUnmount中保存了所有需要执行销毁的useEffect
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = ((unmountEffects[i]: any): HookEffect);
const fiber = ((unmountEffects[i + 1]: any): Fiber);
const destroy = effect.destroy;
effect.destroy = undefined;
if (typeof destroy === 'function') {
// 销毁函数存在则执行
try {
destroy();
} catch (error) {
captureCommitPhaseError(fiber, error);
}
}
}
其中pendingPassiveHookEffectsUnmount数组的索引i保存需要销毁的effect,i+1保存该effect对应的fiber
4.1.2 回调函数的执行
遍历数组,执行对应effect的回调函数
// pendingPassiveHookEffectsMount中保存了所有需要执行回调的useEffect
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {
const effect = ((mountEffects[i]: any): HookEffect);
const fiber = ((mountEffects[i + 1]: any): Fiber);
try {
const create = effect.create;
effect.destroy = create();
} catch (error) {
captureCommitPhaseError(fiber, error);
}
}
5. useRef
ref是reference(引用)的缩写。在React中,我们习惯用ref保存DOM。
对于useRef(1),memoizedState保存{current: 1}
5.1 useRef的两个状态
在mount和update时对应了两个dispatcher
function mountRef<T>(initialValue: T): {|current: T|} {
// 获取当前useRef hook
const hook = mountWorkInProgressHook();
// 创建ref
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
function updateRef<T>(initialValue: T): {|current: T|} {
// 获取当前useRef hook
const hook = updateWorkInProgressHook();
// 返回保存的数据
return hook.memoizedState;
}
useRef仅仅是返回一个包含current属性的对象,可以看React.createRef,证明了ref在mount时只有current属性
export function createRef(): RefObject {
const refObject = {
current: null,
};
return refObject;
}
5.2 Ref工作流程
在React中,HostComponent、ClassComponent、ForwardRef可以赋值ref属性。
// HostComponent
<div ref={domRef}></div>
// ClassComponent / ForwardRef
<App ref={cpnRef} />
其中,ForwardRef只是将ref作为第二个参数传递下去, 不会进入ref的工作流程。
因为HostComponent在commit阶段的mutation阶段执行DOM操作。
所以,对应ref的更新也是发生在mutation阶段。
同时,mutation阶段执行DOM操作的依据为effectTag。
所以,对于HostComponent、ClassComponent如果包含ref操作,那么也会赋值相应的effectTag。
// ...
export const Placement = /* */ 0b0000000000000010;
export const Update = /* */ 0b0000000000000100;
export const Deletion = /* */ 0b0000000000001000;
export const Ref = /* */ 0b0000000010000000;
// ...
所以,ref的工作流程可以分为两部分:
- render阶段为含有ref属性的fiber添加Ref effectTag
- commit阶段为包含Ref effectTag的fiber执行对应操作
5.3 render阶段
在render阶段的beginWork与completeWork中有个同名方法markRef用于为含有ref属性的fiber增加RefeffectTag
// beginWork的markRef
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// Schedule a Ref effect
workInProgress.effectTag |= Ref;
}
}
// completeWork的markRef
function markRef(workInProgress: Fiber) {
workInProgress.effectTag |= Ref;
}
在beginWork中,如下两处调用了markRef:
- updateClassComponent内的finishClassComponent,对应ClassComponent;
注意 ClassComponent 即使 shouldComponentUpdate 为false该组件也会调用markRef
- updateHostComponent,对应HostComponent;
在completeWork中,如下两处调用了markRef:
- completeWork中的HostComponent类型
- completeWork中的ScopeComponent类型
总结下组件对应fiber被赋值Ref effectTag需要满足的条件:
- fiber类型为HostComponent、ClassComponent
- 对于mount,workInProgress.ref !== null,即存在ref属性
- 对于update,current.ref !== workInProgress.ref,即ref属性改变
5.4 commit阶段
在commit阶段的mutation阶段中,对于ref属性改变的情况,需要先移除之前的ref。
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// ...
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
// 移除之前的ref
commitDetachRef(current);
}
}
// ...
}
// ...
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') {
// function类型ref,调用他,传参为null
currentRef(null);
} else {
// 对象类型ref,current赋值为null
currentRef.current = null;
}
}
}
接下来进入ref的赋值阶段,commitLayoutEffect会执行commitAttachRef(赋值ref)
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
// 获取ref属性对应的Component实例
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
// 赋值ref
if (typeof ref === 'function') {
ref(instanceToUse);
} else {
ref.current = instanceToUse;
}
}
}
6. useMemo与useCallback
6.1 mount
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
// 创建并返回当前hook
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 计算value
const nextValue = nextCreate();
// 将value与deps保存在hook.memoizedState
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 创建并返回当前hook
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 将value与deps保存在hook.memoizedState
hook.memoizedState = [callback, nextDeps];
return callback;
}
6.2 update
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
// 返回当前hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
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;
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 返回当前hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// 判断update前后value是否变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 未变化
return prevState[0];
}
}
}
// 变化,将新的callback作为value
hook.memoizedState = [callback, nextDeps];
return callback;
对于update,这两个hook的唯一区别也是回调函数本身还是回调函数的执行结果作为value。