[React] 解析 React 框架底层源码(中)
温馨提示:在写这篇文章的时候其实是图文并茂的,但由于图片都保存在第三方平台的图床中(notion, juejin),搬运到博客也较为麻烦一些,所以博文中就没有图片,如果对图片感兴趣的小伙伴,可以看我的掘金文章,那里有图文并茂的源码解释
目录
1. useEffect | useLayoutEffect
2. useRef
3. useContext
4. useMemo | useCallback
5. useState | useReducer
6. ErrorBoundary | Suspense
useEffect | useLayoutEffect
useEffect 原理
问题: 下面这段代码,从挂载到更新发生了什么?怎么挂载的?怎么更新的。
function Counter() {
useEffect(() => {
console.log("useEffect1");
return () => {
console.log("destroy useEffect1");
};
});
useEffect(() => {
console.log("useEffect2");
return () => {
console.log("destroy useEffect2");
};
});
useEffect(() => {
console.log("useEffect3");
return () => {
console.log("destroy useEffect3");
};
});
return <div></div>;
}> 这里,我们直接进入到 reconciler 阶段,默认已经通过深度优先调度到了 Counter 函数组件的 Fiber节点
useEffect mount 挂载阶段
第一:判断是函数节点的 tag 之后,调用 renderWithHooks.
/*
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate
*/
let value = renderWithHooks(_current,workInProgress,Component);第二:在 renderWithHooks 当中调用 Counter 函数
let children = Component();第三: 调用 Counter 函数 的 useEffect 函数
export function useEffect(create, deps) {
return ReactCurrentDispatcher.current.useEffect(create, deps);
}第四:挂载阶段 ReactCurrentDispatcher.current.useEffect 实则是调用了 mountEffect, moutEffect 调用了 mountEffectImpl
return mountEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps
);第五:在 mountEffectImpl 中创建 调用 mountWorkInProgressHook 和 pushEffect 方法
javascript
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps
);
}
mountWorkInProgressHook 函数,创建 useEffect 的 Hook 对象,构建 fiber.memoizedState 也就是 Hook 链表
```javascript
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;
}
``
pushEffect函数, 创建effect对象 ,赋值给hook.memoizedState,构建fiber.updateQueue队列,队列当中就是一个一个的effect` 对象。
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}前五步已经初始化 mount 完毕,第二个 和 第三个 都是按照此流程,得到了以下的结果。
第六:等到 renderer 阶段中的 beforeMutaion 阶段,将 flushPassiveEffectsImpl 异步调度
function performSyncWorkOnRoot(root) {
flushPassiveEffects();
commitRoot(
root,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
);
return null;
}flushPassiveEffectsImpl函数:
function flushPassiveEffectsImpl() {
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current, lanes, transitions);
return true;
}第七:在 renderer 阶段完成之后(Dom 已经完成挂载),flushPassiveEffectsImpl函数 中 调用 commitPassiveUnmountEffects commitPassiveMountEffects,经过一系列的函数调用到达 commitHookEffectListMount, commitHookEffectListUnmount 执行传入 useEffect 的 mount 函数和 返回的 unMount 函数。(挂载时 unMount 函数为 null)
commitHookEffectListMount函数: 遍历 fiber.updateQueue 以此执行传入 useEffect 的 create 函数
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
effect.destroy = create();
effect = effect.next;
} while (effect !== firstEffect);
}
}commitHookEffectListUnmount 函数,执行 返回的 unMount 函数
function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
// 在此函数中 调用 distroy
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
effect = effect.next;
} while (effect !== firstEffect);
}
}自此 useEffect 挂载阶段执行完毕
useEffect update 更新阶段
第一:判断是函数节点的 tag 之后,调用 renderWithHooks.
/*
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate
*/
let value = renderWithHooks(_current,workInProgress,Component);第二:在 renderWithHooks 当中调用 Counter 函数
let children = Component();第三: 调用 Counter 函数的第一个 useEffect 函数
export function useEffect(create, deps) {
return ReactCurrentDispatcher.current.useEffect(create, deps);
}第四:更新阶段 ReactCurrentDispatcher.current.useEffect 实则是调用了 updateEffect, moutEffect 调用了 updateEffectImpl
return updateEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps
);第五:updateEffectImpl updateWorkInProgressHook 复用 currentHook 的属性 创建新的 Hook 对象 以及 新的 Hook 链表, 调用 areHookInputsEqual 函数判断依赖是否发生变化,没有变化直接 return ,有变化调用 pushEffects
function updateEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}pushEffect 函数 ,创建 effect 对象 ,赋值给 hook.memoizedState,构建 fiber.updateQueue队列,队列当中就是一个一个的 effect 对象。
第六:等到 renderer 阶段中的 beforeMutaion 阶段(commitRoot 之前),将 flushPassiveEffectsImpl 异步调度
function performSyncWorkOnRoot(root) {
flushPassiveEffects();
commitRoot(
root,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
);
return null;
}flushPassiveEffectsImpl函数:
function flushPassiveEffectsImpl() {
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current, lanes, transitions);
return true;
}第七:在 renderer 阶段完成之后(layout 阶段结束之后),flushPassiveEffectsImpl函数 中 调用 commitPassiveUnmountEffects commitPassiveMountEffects,经过一系列的函数调用到达 commitHookEffectListMount, commitHookEffectListUnmount 执行传入 useEffect 的 mount 函数和 返回的 unMount 函数。(更新的时候执行的 destroy 函数是上一次的 useEffect 的返回的函数)
// 更新时候 调用 pushEffects destroy 已经有值,是上一次的 useEffect 的返回的函数
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
effect.destroy = create();
effect = effect.next;
} while (effect !== firstEffect);
}
}commitHookEffectListUnmount 函数,执行 返回的 unMount 函数
function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
// 在此函数中 调用 distroy
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
effect = effect.next;
} while (effect !== firstEffect);
}
}自此 useEffect 更新完毕
useLayoutEffect 原理
useLayoutEffect mount 挂载阶段
第一:判断是函数节点的 tag 之后,调用 renderWithHooks.
/*
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate
*/
let value = renderWithHooks(_current,workInProgress,Component);第二:在 renderWithHooks 当中调用 Counter 函数
let children = Component();第三: 调用 Counter 函数的第一个 useLayoutEffect 函数
export function useLayoutEffect(create, deps) {
return ReactCurrentDispatcher.current.useLayoutEffect(create, deps);
}第四:挂载阶段 ReactCurrentDispatcher.current.useEffect 实则是调用了 mountLayoutEffect, mountLayoutEffect 调用了 mountLayoutEffect
第五:在 mountEffectImpl 中创建 调用 mountWorkInProgressHook 和 pushEffect 方法
javascript
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
mountWorkInProgressHook 函数,创建 useLayoutEffect 的 Hook 对象,构建 fiber.memoizedState 也就是 Hook 链表
```javascript
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;
}
``
pushEffect函数, 创建effect对象 ,赋值给hook.memoizedState,构建fiber.updateQueue队列,队列当中就是一个一个的effect` 对象。
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}第六:等到 renderer 阶段中的 Mutation(Dom 挂载中阶段) 阶段,同步调用 layoutEffect 的 destroy 函数。
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
// 同步调用 layoutEffect 的 destroy 函数
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
// 同步调用 layoutEffect 的 destroy 函数
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
}第七:等到 renderer 阶段中的 Layout (Dom 挂载后) 阶段,同步调用 useLayoutEffect 的 create 函数。
function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
// 同步调用 useLayoutEffect 的 create 函数。
commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
// 同步调用 useLayoutEffect 的 create 函数。
commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}自此 useLayoutEffect 挂载阶段执行完毕
useLayoutEffect update 更新阶段
第一:判断是函数节点的 tag 之后,调用 renderWithHooks.
/*
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate
*/
let value = renderWithHooks(_current,workInProgress,Component);第二:在 renderWithHooks 当中调用 Counter 函数
let children = Component();第三: 调用 Counter 函数的第一个 useEffect 函数
export function useEffect(create, deps) {
return ReactCurrentDispatcher.current.useEffect(create, deps);
}第四:更新阶段 ReactCurrentDispatcher.current.useEffect 实则是调用了 updateEffect, moutEffect 调用了 updateEffectImpl
return updateEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps
);第五:updateEffectImpl 函数: updateWorkInProgressHook 函数 复用 currentHook 的属性 创建新的 Hook 对象 以及 新的 Hook 链表, 调用 areHookInputsEqual 函数判断依赖是否发生变化,没有变化直接 return ,有变化调用 pushEffects
function updateEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}第六:等到 renderer 阶段中的 Mutation(Dom 挂载中阶段) 阶段,同步调用 layoutEffect 的 destroy 函数。
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
// 同步调用 layoutEffect 的 destroy 函数
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
// 同步调用 layoutEffect 的 destroy 函数
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
}第七:等到 renderer 阶段中的 Layout (Dom 挂载后) 阶段,同步调用 useLayoutEffect 的 create 函数。
function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
// 同步调用 useLayoutEffect 的 create 函数。
commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
// 同步调用 useLayoutEffect 的 create 函数。
commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}自此 useLayoutEffect 更新阶段执行完毕
useRef
读完收获
ref 怎么实现获取 Dom 引用的?
怎么实现 useRef 可以得到一个可变且不刷新页面的值?
如果 ref.current 改变后,会刷新页面,如果你是 React 开发者,如何设计?
useRef 使用
useRef ref 值作为 DOM 引用
案例 1:使用 ref focus input
function App() {
return <Counter label="Label" value="Value" isFocus />;
}
function Counter({ label, value, isFocus }) {
const ref = React.useRef(); // (1)
React.useEffect(() => {
if (isFocus) {
ref.current.focus(); // (3)
}
}, [isFocus]);
return (
<label>
{/* (2) */}
{label}: <input type="text" value={value} ref={ref} />
</label>
);
}useRef 作为可变值:
案例 2: 判断组件是首次渲染还是重新渲染
function Counter() {
const [count, setCount] = React.useState(0);
function onClick() {
setCount(count + 1);
}
const isFirstRender = React.useRef(true);
React.useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
console.log(
`
I am a useEffect hook's logic
which runs for a component's
re-render.
`
);
}
});
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
</div>
);
}
复制代码useRef 原理
> 这里,我们直接进入到 reconciler 阶段,默认已经通过深度优先调度到了 Counter 函数组件的 Fiber节点
useRef mount 挂载阶段
第一:判断是函数节点的 tag 之后,调用 renderWithHooks.
/*
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate
*/
let value = renderWithHooks(_current,workInProgress,Component);第二:在 renderWithHooks 当中调用 Counter 函数
let children = Component();第三: 调用 Counter 函数 的 useRef 函数
export function useRef(reducer, initialArg) {
return ReactCurrentDispatcher.current.useRef(reducer, initialArg);
}第四:挂载阶段 ReactCurrentDispatcher.current.useRef 实则是调用了 mountRef,
function mountRef<T>(initialValue: T): { current: T } {
const hook = mountWorkInProgressHook();
const ref = { current: initialValue };
hook.memoizedState = ref;
return ref;
}第五:在 mountRef 中调用,mountWorkInProgressHook 函数,创建 useRef 的 Hook 对象,构建 fiber.memoizedState 也就是 Hook 链表, 创建 ref 对象,里面有 current 属性,useRef 的 Hook.memoizedState 属性 就是 该 ref 对象。 返回 ref 对象。组件当中可以通过 ref.current 拿到该初始值。
第六:reconciler 阶段执行之后,来到 render 阶段 中的 layout 阶段,在 commitLayoutEffectOnFiber当中执行 safelyAttachRef 再执行 commitAttachRef
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
safelyAttachRef(finishedWork, finishedWork.return);
}第七: commitAttachRef 判断 ref 挂载的类型, 原生节点,通过 fiber 节点 的 stateNode 属性获取到原生节点,将 原生 DOM 节点 赋值给 ref.current。
通过第七点可以解决问题1:如何绑定 Dom 节点?
render 阶段中的 mutation 阶段 Dom 已经挂载了, layout 阶段可以 通过 Fiebr.stateNode 获取到 新的 原生 Dom 节点,useEffect 是在 render 阶段之后执行的,所以 这时候通过 ref.current 可以 获取到 原生 Dom 节点。
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostResource:
case HostSingleton:
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
ref.current = instanceToUse;
}
}如果之后再有 useState useReducer,最终mout阶段的成果是
自此 useRef 挂载阶段执行完毕
useRef update 更新阶段
第一:我们直接进入到 reconciler 阶段,默认已经通过深度优先更新调度到了 Counter 函数组件的 Fiber节点
/*
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate
*/
let value = renderWithHooks(_current,workInProgress,Component);第三:在 renderWithHooks 当中调用 Counter 函数
let children = Component();第四: 调用 Counter 函数 的 useRef 函数
export function useRef(reducer, initialArg) {
return ReactCurrentDispatcher.current.useRef(reducer, initialArg);
}第五: 更新阶段 ReactCurrentDispatcher.current.useReducer 实则是调用了 updateRef
function updateRef<T>(initialValue: T): {current: T} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}第六:在 updateRef 中调用 updateWorkInProgressHook 函数,在此函数中最重要的就是通过 alternate 指针复用 currentFiber(老 Fiber) 的 memorizedState, 也就是 Hook 链表,并且按照严格的对应顺序来复用 currentFiber(老 Fiber) Hook 链表当中的 Hook(通过 currentHook 指针结合链表来实现),通过尽可能的复用来创建新的 Hook 对象,构建 fiber.memoizedState 也就是新的 Hook 链表。
第七: updateRef 直接将 hook.memoizedState 返回,也就是 ref 对象 ref = {current: }。
通过第七点可以解决问题2:useRef 如何作为可变值?
组件当中 直接通过
ref.current = "true"修改 ref 对象的 current 属性,current 通过对象引用的修改, 已经最新值了, 但是 React 没有像 useReducer 中 dispatch 的时候去 调用 scheduleFiberOnRoot 从根节点开始遍历,刷新页面,所以 ref.current 的值 在 UI 当中不会刷新,直至下一次 dispatch 或者 setState 才会刷新。因为函数重新执行了,又调用了 useRef -> updateRef 拿到最新值。
如果我们想让 ref.current 修改的时候页面刷新,该怎么办,笔者的想法是 通过 Object.definedProperty 中定义 settter getter ,修改的时候 调用 setter 函数, 在 setter 函数中 调用 scheduleFiebrOnRoot
再次从 根节点开始深度遍历,重新经过 scheduler reconciler render 三个大阶段就可以实现刷新页面。
自此 useRef 更新完毕
useContext
读完收获
当我们 React.creataContext() 时 发生了什么?
Context.Provider 是如何挂载与更新的?
useContext 是怎么样获取到上下文对象的?
从下面的代码开始入手分析,下面代码完成的是点击文字后随机修改文字颜色。
import * as React from "react";
import { createContext, useState, useContext } from "react";
import ReactDOM from "react-dom";
const genColor = () => `hsla(${Math.random() * 360}, 70%, 50%)`;
type MyContext = { color: string, changer: (any) => void };
const ThemeContext = createContext < MyContext > null;
const Comp = () => {
const { color, changer } = useContext(ThemeContext);
return (
<button style={{ color, fontSize: "2em" }} onClick={changer}>
Hello World! Click to change color
</button>
);
};
const App = () => {
const [state, setState] = useState({ color: "green" });
const { color } = state;
const changer = () => setState({ color: genColor() });
return (
<ThemeContext.Provider value={{ color, changer }}>
<Comp />
</ThemeContext.Provider>
);
};Mount 阶段
第一步:首先通过 createContext 创建了 context 对象。我们传入的 defaultValue 会被当作 context 对象的 __currentValue 属性,同时 我们也可以发现 context.Provider 是一个 React 元素。 Provider 的 __context 属性指向了 context 对象。
export function createContext<T>(defaultValue: T): ReactContext<T> {
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
_currentValue: defaultValue,
_currentValue2: defaultValue,
_threadCount: 0,
Provider: (null: any),
Consumer: (null: any),
_defaultValue: (null: any),
_globalName: (null: any),
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
context.Consumer = context;
return context;
}第二步:这里我们默认 reconciler 阶段 深度优先调度遍历到了 App Fiber 节点,创建了颜色状态,改变颜色状态的函数。传入 Context.Provider 组件的 props 当中。
const [state, setState] = useState({ color: "green" });
const { color } = state;
const changer = () => setState({ color: genColor() });
return (
<ThemeContext.Provider value={{ color, changer }}>
<Comp />
</ThemeContext.Provider>
);第三步:默认走到 reconciler 阶段, beginWork 函数中开始挂载 Context.Provider,调用updateContextProvider 函数
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
switch (workInProgress.tag) {
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
}
}第四步:updateContextProvider 函数当中调用 pushProvider 函数。
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
) {
/* 这里的 workInProgress.type 就是 Provider.type = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
}; */
const providerType: ReactProviderType<any> = workInProgress.type;
// providerType._context; 就是 Provider.type.context
const context: ReactContext<any> = providerType._context;
const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps;
const newValue = newProps.value;
pushProvider(workInProgress, context, newValue);
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
let hasWarnedAboutUsingContextAsConsumer = false;第五步: pushProvider 函数当中将 newValue 也就是 <ThemeContext.Provider value={{ color, changer }}> 的 props 值 赋值给 context.currentValue。现在 `context.currentValue = { color, changer }`
export function pushProvider<T>(
providerFiber: Fiber,
context: ReactContext<T>,
nextValue: T
): void {
context._currentValue = nextValue;
context._currentValue2 = nextValue;
}第六步:默认 reconciler 阶段深度优先调度遍历到了 Comp Fiber 节点, beginWork 函数判断 workInProgress.tag根据 tag 进行挂载,函数式组件调用 renderWithHooks,在 renderWithHooks 函数当中 调用 Comp 函数。继续调用 useContext(ThemeContext)
const Comp = () => {
const { color, changer } = useContext(ThemeContext);
return (
<button style={{ color, fontSize: "2em" }} onClick={changer}>
Hello World! Click to change color
</button>
);
};第七步:调用 useContext 并传入 ThemeContext,返回 context.__currentVale, 由于第五步挂载 <Context.Provider/> 的时候,已经将 context.__currntValue 修改成了 {color:"green", changer: () => setState(randomColor)},所以该对象被返回。
function useContext<T>(context: ReactContext<T>): T {
return context._currentValue;
}第八步:Comp 返回新的虚拟 Dom,去 reconcilChildren…… reconciler 阶段结束 -> render 阶段完成之后,真实 Dom 挂载,文字颜色就是上下文对象提供的 color。
return (
<button style={{ color, fontSize: "2em" }} onClick={changer}>
Hello World! Click to change color
</button>
);自此 Context 挂载阶段完成。
Update 阶段
第一步:点击按钮,出发 changer函数,changer 函数则调用了 setState。
const App = () => {
const [state, setState] = useState({ color: "green" });
const { color } = state;
const changer = () => setState({ color: genColor() });
return (
<ThemeContext.Provider value={{ color, changer }}>
<Comp />
</ThemeContext.Provider>
);
};第二步:(这里不展开,setState的细节)我们 默认已经又来到了 reconciler 阶段的 App Fiber 节点,又开始调用App函数,调用 useState -> updateState, 得到了 新的颜色状态。传入 Context.Provider 组件的 props 当中。
const [state, setState] = useState({ color: "green" });
const { color } = state;
const changer = () => setState({ color: genColor() });
return (
<ThemeContext.Provider value={{ color, changer }}>
<Comp />
</ThemeContext.Provider>
);第三步: reconciler 阶段, beginWork 函数中开始挂载 Context.Provider 组件调用updateContextProvider 函数
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
switch (workInProgress.tag) {
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
}
}第四步:updateContextProvider 函数当中调用 pushProvider 函数。
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
) {
/* 这里的 workInProgress.type 就是 Provider.type = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
}; */
const providerType: ReactProviderType<any> = workInProgress.type;
// providerType._context; 就是 Provider.type.context
const context: ReactContext<any> = providerType._context;
const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps;
const newValue = newProps.value;
pushProvider(workInProgress, context, newValue);
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
let hasWarnedAboutUsingContextAsConsumer = false;第五步: pushProvider 函数当中将 newValue 也就是 <ThemeContext.Provider value={{ color, changer }}> 的 props 值 赋值给 context.currentValue。现在 `context.currentValue = { color, changer }`
export function pushProvider<T>(
providerFiber: Fiber,
context: ReactContext<T>,
nextValue: T
): void {
context._currentValue = nextValue;
context._currentValue2 = nextValue;
}第六步:默认 reconciler 阶段深度优先调度遍历到了 Comp Fiber 节点, beginWork 函数判断 workInProgress.tag根据 tag 进行挂载,函数式组件调用 renderWithHooks,在 renderWithHooks 函数当中 调用 Comp 函数。继续调用 useContext(ThemeContext)
const Comp = () => {
const { color, changer } = useContext(ThemeContext);
return (
<button style={{ color, fontSize: "2em" }} onClick={changer}>
Hello World! Click to change color
</button>
);
};第七步:调用 useContext 并传入 ThemeContext,返回 context.__currentVale, 由于第四步挂载 <Context.Provider/> 的时候,已经将 context.__currntValue 修改成了 {color:"新颜色", changer: () => setState(randomColor)},所以该对象被返回。
function useContext<T>(context: ReactContext<T>): T {
return context._currentValue;
}第八步:Comp 返回新的虚拟 Dom,去 reconcilChildren…… reconciler 阶段结束 -> render 阶段完成之后,真实 Dom 更新,文字颜色就是上下文对象提供的更新后的 color。
return (
<button style={{ color, fontSize: "2em" }} onClick={changer}>
Hello World! Click to change color
</button>
);自此 Context 更新阶段完成。
useMemo | useCallback
useMemo 原理
问题: 下面这段代码,从挂载到更新发生了什么?怎么挂载的?怎么更新的。
const reducer = (state, action) => {
if (action.type === "add") return state + 1;
else return state;
};
function Counter() {
const [state, setState] = useState(0);
const memoFn = useMemo(() => state ** 2, [state]);
return (
<div
onClick={() => {
setState((pre) => pre + 1)
}}
>
{number}
</div>
);
}> 这里,我们直接进入到 reconciler 阶段,默认已经通过深度优先调度到了 Counter 函数组件的 Fiber节点
useMemo mount 挂载阶段
第一:判断是函数节点的 tag 之后,调用 renderWithHooks.
/*
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate
*/
let value = renderWithHooks(_current,workInProgress,Component);第二:在 renderWithHooks 当中调用 Counter 函数
let children = Component();第三: 调用 Counter 函数 的 useMemo 函数
export function useMemo(reducer, initialArg) {
return ReactCurrentDispatcher.current.useMemo(create, deps);
}第四:挂载阶段 ReactCurrentDispatcher.current.useMemo 实则是调用了 mountMemo,
第五:在 mountMemo 中调用,mountWorkInProgressHook 函数,创建 useMemo 的 Hook 对象,构建 fiber.memoizedState 也就是 Hook 链表。将我们传入的函数进行计算作为 nextValue,并且返回。组件当中可以消费计算值。
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;
}如果之后再有 useState useReducer,最终mout阶段的成果是
自此 useMemo 挂载阶段执行完毕
useMemo update 更新阶段
第一:更新阶段 ReactCurrentDispatcher.current.useMemo 实则是调用了 updateMemo函数
第二:在 updateMemo 中调用 updateWorkInProgressHook 函数,在此函数中最重要的就是通过 alternate 指针复用 currentFiber(老 Fiber) 的 memorizedState, 也就是 Hook 链表,并且按照严格的对应顺序来复用 currentFiber(老 Fiber) Hook 链表当中的 Hook(通过 currentHook 指针结合链表来实现)。
第三:通过 areHookInputsEqual 函数,来判断 deps 依赖是否发送了变化,如果没有发送变化,直接返回缓存的 memo 值,如果发生了变化,调用 nextCreate 函数,重新计算 memo 值,并返回。
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];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate();
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}areHookInputsEqual 函数,就是遍历新,旧依赖。通过 Obejct.is 来判断依赖是否相等。
```javascript
function areHookInputsEqual(
nextDeps: Array
return true; } ```
自此 useMemo 更新完毕
useCallback 原理
问题: 下面这段代码,从挂载到更新发生了什么?怎么挂载的?怎么更新的。
const reducer = (state, action) => {
if (action.type === "add") return state + 1;
else return state;
};
function Counter() {
const [state, setState] = useState(0);
const memoFn = useCallback(() => state ** 2, [state]);
return (
<div
onClick={() => {
setState((pre) => pre + 1)
}}
>
{number}
</div>
);
}> 这里,我们直接进入到 reconciler 阶段,默认已经通过深度优先调度到了 Counter 函数组件的 Fiber节点
useCallback mount 挂载阶段
第一:判断是函数节点的 tag 之后,调用 renderWithHooks.
/*
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate
*/
let value = renderWithHooks(_current,workInProgress,Component);第二:在 renderWithHooks 当中调用 Counter 函数
let children = Component();第三: 调用 Counter 函数 的 useCallback 函数
export function useMemo(reducer, initialArg) {
return ReactCurrentDispatcher.current.useCallback(create, deps);
}第四:挂载阶段 ReactCurrentDispatcher.current.useMemo 实则是调用了 mountCallback,
第五:在 mountCallback 中调用,mountWorkInProgressHook 函数,创建 useCallback 的 Hook 对象,构建 fiber.memoizedState 也就是 Hook 链表。将我们传入的函数返回。组件当中可以消费该函数。
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}如果之后再有 useState useReducer,最终mout阶段的成果是
自此 useMemo 挂载阶段执行完毕
useCallback update 更新阶段
第一:更新阶段 ReactCurrentDispatcher.current.useMemo 实则是调用了 updateMemo函数
第二:在 updateMemo 中调用 updateWorkInProgressHook 函数,在此函数中最重要的就是通过 alternate 指针复用 currentFiber(老 Fiber) 的 memorizedState, 也就是 Hook 链表,并且按照严格的对应顺序来复用 currentFiber(老 Fiber) Hook 链表当中的 Hook(通过 currentHook 指针结合链表来实现)。
第三:通过 areHookInputsEqual 函数,来判断 deps 依赖是否发送了变化,如果依赖没有发送变化,直接返回缓存的 函数,如果依赖发生了变化,则返回新的函数。
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
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];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}areHookInputsEqual 函数,就是遍历新,旧依赖。通过 Obejct.is 来判断依赖是否相等。
```javascript
function areHookInputsEqual(
nextDeps: Array
return true; } ```
自此 useCallback 更新完毕
ErrorBoundary | Suspense
ErrorBoundary
React 通过深度优先遍历来调度 Fiber ,所以 ErrorBoundary 组件 有能力捕获到渲染期间子组件发生的异常,并且会阻止异常继续传播,不会导致整个应用程序崩溃。
export default class ErrorBoundary extends React.Component<Props> {
state = { hasError: false, error: null };
static getDerivedStateFromError (error: any) {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}当子组件发生异常时,会通过调用 getNearestErrorBoundaryID 找到距离发生异常的子组件,最近的 ErrorBoundary 组件,调用 getDerivedStateFromError 方法,将 this.state.hasError 置为 true, 则渲染到 ErrorBoundary 组件时,调用 render 函数,可以利用 hasError 组件状态,在 render 函数中定制子组件发生异常时的 UI。
function getNearestErrorBoundaryID(fiber: Fiber): number | null {
let parent = fiber.return;
while (parent !== null) {
if (isErrorBoundary(parent)) {
return getFiberIDUnsafe(parent);
}
parent = parent.return;
}
return null;
}Suspense
Suspense 的核心概念与 ErrorBoundary 非常相似,ErrorBoundary 是通过捕获其子组件的异常,来展示错误回滚的 UI。而 Suspense 是通过捕获子组件 抛出的 promise 来展示数据获取之前的 fallBack UI,数据获取之后展示 children UI。
Suspense 解决问题是:极大简化复杂的 stuff, 比如 异步获取数据 (data fetching), 代码分割 (code splitting),以及在你的应用程序当中任何异步依赖的数据。Suspense 可以精准控制异步数据/组件的 Loading状态。
Suspense 在 异步组件中的使用,就是和 React.lazy 结合,在异步组件获取到之前,显示 fallback UI。
export function lazy<T>(
ctor: () => Thenable<{default: T, ...}>,
): LazyComponent<T, Payload<T>> {
const payload: Payload<T> = {
// We use these fields to store the result.
_status: Uninitialized,
_result: ctor,
};
}
React.lazy原理相当于就是在组件当中throw了一个thenable函数。
Suspense 在 Loading 中的使用,大家可以去看 React 核心成员 Dan 的 demo。Dan Abramov 的 frosty-hermann-bztrp
Suspense Loading
Suspense 使用
export default class extends Component {
render() {
return (
<Suspense fallback={<h1>加载中</h1>}>
<User />
</Suspense>
</ErrorBoundary>
);
}
}User 组件在数据获取到之前,即状态为 pending 时会 throw 一个 promise。
// Suspense 参数就是一个发送网络请求的 promise
function wrapperPromise(Suspense: Promise<any>) {
let status = "pending";
let result: any;
return {
read() {
if (status === "success" || status === "error") {
return result;
} else {
// throw 一个 promise
throw promise.then(
(data: any) => {
status = "success";
result = data;
},
(error: any) => {
status = "error";
result = error;
}
);
}
},
};
}这时, 距离该子组件最近的 Suspense 组件 componentDidCatch 生命周期中,捕获到了该 promise。将状态 loading 置为 true, 调度更新到 Suspense 组件时执行 render 函数时, loading 为 true 渲染 fallBack UI,跳过子组件。
export default class Suspense extends React.Component<
SuspenseProps,
SuspenseState
> {
mounted: any = null;
state = { loading: false };
componentDidCatch(error: any) {
if (typeof error.then === "function") {
this.setState({ loading: true });
error.then(() => {
this.setState({ loading: false });
});
}
}
render() {
const { fallback, children } = this.props;
const { loading } = this.state;
return loading ? fallback : children;
}
}等到 User 组件在数据获取到之后, 执行 then 回调函数中将状态 loading 置为 false, 再次调度更新时,loaing 为 true 会直接渲染子组件。
componentDidCatch(error: any) {
if (typeof error.then === "function") {
this.setState({ loading: true });
error.then(() => {
this.setState({ loading: false });
});
}
}