[React] 解析 React 框架底层源码(中)

2023 年 3 月 1 日 星期三(已编辑)
2

[React] 解析 React 框架底层源码(中)

温馨提示:在写这篇文章的时候其实是图文并茂的,但由于图片都保存在第三方平台的图床中(notion, juejin),搬运到博客也较为麻烦一些,所以博文中就没有图片,如果对图片感兴趣的小伙伴,可以看我的掘金文章,那里有图文并茂的源码解释

链接: https://juejin.cn/column/7149818417325801503

目录

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 中创建 调用 mountWorkInProgressHookpushEffect 方法 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 函数,创建 useEffectHook 对象,构建 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 执行传入 useEffectmount 函数和 返回的 unMount 函数。(挂载时 unMount 函数为 null)

commitHookEffectListMount函数: 遍历 fiber.updateQueue 以此执行传入 useEffectcreate 函数

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 执行传入 useEffectmount 函数和 返回的 unMount 函数。(更新的时候执行的 destroy 函数是上一次的 useEffect 的返回的函数)

// 更新时候 调用 pushEffects  destroy 已经有值,是上一次的 useEffect 的返回的函数
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
`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 更新完毕

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 中创建 调用 mountWorkInProgressHookpushEffect 方法 javascript const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushEffect( HookHasEffect | hookFlags, create, undefined, nextDeps, ); mountWorkInProgressHook 函数,创建 useLayoutEffectHook 对象,构建 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 函数,创建 useRefHook 对象,构建 fiber.memoizedState 也就是 Hook 链表, 创建 ref 对象,里面有 current 属性,useRefHook.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节点

第二:判断是函数节点的 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.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 函数,创建 useMemoHook 对象,构建 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, prevDeps: Array | null ): boolean { for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { if (is(nextDeps[i], prevDeps[i])) { continue; } return false; }

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 函数,创建 useCallbackHook 对象,构建 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, prevDeps: Array | null ): boolean { for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { if (is(nextDeps[i], prevDeps[i])) { continue; } return false; }

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 函数。

SuspenseLoading 中的使用,大家可以去看 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 函数时, loadingtrue 渲染 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, 再次调度更新时,loaingtrue 会直接渲染子组件。

componentDidCatch(error: any) {
  if (typeof error.then === "function") {
    this.setState({ loading: true });
    error.then(() => {
      this.setState({ loading: false });
    });
  }
}

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...