[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 });
});
}
}