Skip to content

前言

React 18中,我们详细分析了各种类型,比如HostComponentFunctionComponent的初次渲染和渲染更新的流程,但是我们还没仔细分析过React重要的React Hooks相关源码

  • 比如最常见的useEffect()
  • 比如提供全局数据的useContext()
  • 比如性能优化经常使用的useMemo()useCallback()

在本文中,我们将针对这些常见的hooks,侧重于useEffect()useLayoutEffect(),详细分析React Hooks初次渲染和渲染更新的流程

整体流程图(TODO)


1. useEffect()

从下面useEffect()的使用中,我们可以知道,主要分为两个部分

  • ()=> {}: 回调函数
  • [color]: 依赖项数组
ts
useEffect(() => {
  console.log("useEffect 执行: 当前颜色", color);
  // 模拟一个可能引起布局变化的操作(但会有延迟)
  const element = document.getElementById("box");
  if (element) {
    element.style.transform = "translateX(100px)";
  }
  return () => {
    console.log("Effect destroyed");
  };
}, [color]);

而从React 18的源码中,我们可以发现,跟useState()类似,useEffect()也会根据初始化或者更新时选择不同的方法

ts
var HooksDispatcherOnMount = {
  //...
  useEffect: mountEffect,
  useLayoutEffect: mountLayoutEffect,
  //...
};
var HooksDispatcherOnUpdate = {
  //...
  useEffect: updateEffect,
  useLayoutEffect: updateLayoutEffect,
  //...
};

初始化时,useEffect本质就是mountEffect()

更新时,useEffect本质就是updateEffect()

ts
function mountEffect(create, deps) {
  return mountEffectImpl(Passive | PassiveStatic, Passive$1, create, deps);
}
function updateEffect(create, deps) {
  return updateEffectImpl(Passive, Passive$1, create, deps);
}

mountEffectImpl()updateEffectImpl()的代码可以知道,其实本质都是

  • 使用mountWorkInProgressHook()或者updateWorkInProgressHook()获取当前的hook
  • 更新阶段使用areHookInputsEqual(nextDeps, prevDeps)判断依赖是否已经改变,如果改变,则打上HasEffect|Passiveflags,为后面的commit阶段做准备;如果依赖项没有改变,则打上flagsPassive,为后面的commit阶段做准备
  • 最终还是需要执行一次pushEffect()重新构建一次fiber.updateQueue单循环链表结构

区别于依赖项改变,则传入pushEffect()hookFlags不同

在下面的小节,我们将针对一些方法进行详细的分析

ts
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber$1.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HasEffect | hookFlags,
    create,
    undefined,
    nextDeps
  );
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
  var hook = updateWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var destroy = undefined;
  if (currentHook !== null) {
    var prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      var prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  currentlyRenderingFiber$1.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HasEffect | hookFlags,
    create,
    destroy,
    nextDeps
  );
}

1.1 mountWorkInProgressHook

  • 创建一个新的hook
  • 如果当前fiber的hook集合,也就是fiber.memoizedState这个存储单链表数据为空,则进行头节点的赋值,否则进行workInProgressHook.next = hook构建单链表结构
  • 最终返回当前fiber的hook集合的最后一个元素
ts
function mountWorkInProgressHook() {
  var hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

1.2 updateWorkInProgressHook

hooks&更新渲染流程分析的文章分析中,我们已经分析过updateWorkInProgressHook()获取当前fiber的hook

源码的代码过于繁琐和难以看懂,可以简化为下面代码

因为是更新阶段,一般alternate是存在的(可能存在之前没有hook,更新后又有hook,那么alternate就不存在)

这里考虑一般情况下=>可以简化代码逻辑,容易理解

由于是更新阶段,因此currentlyRenderingFiber$1.alternate必定存在,复用当前alternatememoizedState构建链表数据,主要是头节点的赋值+剩余节点的赋值两个步骤

涉及到三个全局变量的赋值:

  • currentHook: 代表的是当前renderFiber.alternate对应的hook
  • currentlyRenderingFiber.memoizedState: 当前renderFiber.alternate复制的单链表
  • workInProgressHook: 当前renderFiber对应的hook

fiber.memoizedStatehook.memoizedState是不一样的!!

javascript
function updateWorkInProgressHook() {
	const current = currentlyRenderingFiber.alternate;
	if (workInProgressHook === null) {
		// 头节点还没赋值,获取当前fiber的alternate对应的单循环链表结构,因为是更新阶段,一般alternate是存在的(可能存在之前没有hook,更新后又有hook,那么alternate就不存在)
		currentlyRenderingFiber.memoizedState = current.memoizedState;
		workInProgressHook = currentlyRenderingFiber.memoizedState;
		currentHook = current.memoizedState;
	} else {
        // 当前fiber的下一个hook,因为一个fiber可能存在多个hook,会形成一个单循环链表结构
		workInProgressHook = workInProgressHook.next;
		currentHook = currentHook.next;
	}
	return workInProgressHook;
}

1.3 pushEffect重新构建fiber.updateQueue

传入的参数值中

  • tag: HasEffect | PassiveHasEffect代表依赖项已经改变或者首次渲染,当检测到HasEffect,需要重新执行create()方法;Passive代表该fiber存在hook,在销毁时调用effect.destory()可以通过这个flags进行判断
  • create: useEffect(create, [deps])的执行方法
  • destroy: 在commitHookEffectListMount()中进行destroy=create()赋值(首次渲染阶段不会触发destroy()),它会在调用一个新的effect执行之前对前一个effect进行清理
  • deps: useEffect()的依赖项,依赖项变化时,会重新执行传入的create()方法

destroy()的执行将在下面的小节展开分析

fiber.updateQueue存放着hook的单链表数据,如果为空,则通过createFunctionComponentUpdateQueue()创建出新的fiber.updateQueue

注意: fiber.updateQueue是一个对象,具备lastEffectstores两个属性

如果fiber.updateQueue已经初始化完成,则按照单链表的形式不断next即可

  • lastEffect代表单链表的最后一个hook
  • firstEffect代表单链表的最前面的一个hook
  • lastEffect.next=firstEffect构成循环结构
ts
function pushEffect(tag, create, destroy, deps) {
  var effect = {
    tag: tag,
    create: create,
    destroy: destroy,
    deps: deps,
    next: null,
  };
  var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;

  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    var lastEffect = componentUpdateQueue.lastEffect;

    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      var firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }

  return effect;
}
function createFunctionComponentUpdateQueue() {
  return {
    lastEffect: null,
    stores: null,
  };
}

1.3 commit提交阶段处理effect

commit阶段最终是提交fiber Root的复制fiber进行提交,然后触发finishConcurrentRender()方法,从而最终触发commitRootImpl()方法

javascript
var finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, lanes);

function finishConcurrentRender(root, exitStatus, lanes) {
  switch (exitStatus) {
    //...
    case RootCompleted: {
      commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions);
      break;
    }
  }
}
function commitRoot(root, recoverableErrors, transitions) {
  //...
  commitRootImpl(root, recoverableErrors, transitions, previousUpdateLanePriority);
}

commitRootImpl()的核心代码如下所示:

  • 判断subtreeHasEffects(root元素的children存在effect/存在Placement等flags)和rootHasEffect(root元素存在effect/存在Placement等flags)然后调用
    • commitBeforeMutationEffects()
    • commitMutationEffects()
    • commitLayoutEffects()
  • 调用异步更新ensureRootIsScheduled()
  • 调用同步更新flushSyncCallbacks()
  • 微任务触发flushPassiveEffects()触发effect相关处理
javascript
function commitRootImpl() {
    //...
    if (
        (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
        (finishedWork.flags & PassiveMask) !== NoFlags
    ) {
        if (!rootDoesHavePassiveEffects) {
            rootDoesHavePassiveEffects = true;
            scheduleCallback$2(NormalPriority, function () {
                flushPassiveEffects();
                return null;
            });
        }
    }
    var subtreeHasEffects =
        (finishedWork.subtreeFlags &
            (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
        NoFlags;
    var rootHasEffect =
        (finishedWork.flags &
            (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
        NoFlags;
    if (subtreeHasEffects || rootHasEffect) {
        const prevExecutionContext = executionContext;
        executionContext |= CommitContext;
        commitBeforeMutationEffects(root, finishedWork);
        commitMutationEffects(root, finishedWork, lanes);
        commitLayoutEffects(finishedWork, root, lanes);
        executionContext = prevExecutionContext;
    } else {
        //...no effects
    }

    ensureRootIsScheduled(root, now());
    flushSyncCallbacks();
}

从上面代码可以看出

  • 同步执行了commitLayoutEffects(),关联LayoutEffect相关逻辑
  • 异步执行了flushPassiveEffects(),关联Effect相关逻辑

1.3.1 flushPassiveEffects()

由于flushPassiveEffects()的代码过于繁杂,使用一个流程图进行展示

Image

最终触发的是

  • commitHookEffectListUnmount(): 触发effect.destory()
  • commitHookEffectListMount(): 触发effect.create()

在删除fiber的逻辑中,使用PassiveMask=Passive|ChildDeletion作为判断触发commitHookEffectListUnmount()

在清除上一个effect的逻辑中,使用Passive作为判断来触发commitPassiveUnmountOnFiber()->commitHookEffectListUnmount()

在触发effect.create()时,先使用PassiveMask进行筛选,然后使用Passive作为判断来触发commitPassiveMountOnFiber()

ts
function commitPassiveMountEffects_begin(
  subtreeRoot,
  root,
  committedLanes,
  committedTransitions
) {
  while (nextEffect !== null) {
    var fiber = nextEffect;
    var firstChild = fiber.child;

    if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && firstChild !== null) {
      firstChild.return = fiber;
      nextEffect = firstChild;
    } else {
      commitPassiveMountEffects_complete(
        subtreeRoot,
        root,
        committedLanes,
        committedTransitions
      );
    }
  }
}
function commitPassiveMountEffects_complete(
  subtreeRoot,
  root,
  committedLanes,
  committedTransitions
) {
  while (nextEffect !== null) {
    var fiber = nextEffect;
    if ((fiber.flags & Passive) !== NoFlags) {
      commitPassiveMountOnFiber(
        root,
        fiber,
        committedLanes,
        committedTransitions
      );
    }
    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }
    var sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }
    nextEffect = fiber.return;
  }
}

2. useLayoutEffect()

从下面代码可以看出,useLayoutEffect()useEffect()的初始化流程几乎一模一样,区别在于传入的参数中

  • fiberFlagsUpdate
  • hookFlagsLayout

useEffect()的传入参数为

  • fiberFlagsPassive
  • hookFlagsPassive

注: 两个Passive为不同的数据...虽然名称一模一样

ts
function mountLayoutEffect(create, deps) {
  var fiberFlags = Update;
  //...
  return mountEffectImpl(fiberFlags, Layout, create, deps);
}
function updateLayoutEffect(create, deps) {
  return updateEffectImpl(Update, Layout, create, deps);
}
ts
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
    var hook = mountWorkInProgressHook();
    var nextDeps = deps === undefined ? null : deps;
    currentlyRenderingFiber$1.flags |= fiberFlags;
    hook.memoizedState = pushEffect(HasEffect | hookFlags, create, undefined, nextDeps);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
    var hook = updateWorkInProgressHook();
    var nextDeps = deps === undefined ? null : deps;
    var destroy = undefined;

    if (currentHook !== null) {
        var prevEffect = currentHook.memoizedState;
        destroy = prevEffect.destroy;
        if (nextDeps !== null) {
            var prevDeps = prevEffect.deps;
            if (areHookInputsEqual(nextDeps, prevDeps)) {
                hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
                return;
            }
        }
    }
    currentlyRenderingFiber$1.flags |= fiberFlags;
    hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);
}

2.1 commit提交阶段处理commitLayoutEffects

从上面useEffect()commit阶段分析可以知道,commit阶段最终是提交fiber Root的复制fiber进行提交,然后触发finishConcurrentRender()方法,从而最终触发commitRootImpl()方法

commitRootImpl()的核心代码如下所示:

  • 判断subtreeHasEffects(root元素的children存在effect/存在Placement等flags)和rootHasEffect(root元素存在effect/存在Placement等flags)然后调用
    • commitBeforeMutationEffects()
    • commitMutationEffects()
    • commitLayoutEffects()
  • 调用异步更新ensureRootIsScheduled()
  • 调用同步更新flushSyncCallbacks()
  • 微任务触发flushPassiveEffects()触发effect相关处理
ts
function commitLayoutEffects(finishedWork, root, committedLanes) {
  commitLayoutEffects_begin(finishedWork, root, committedLanes);
}
function commitLayoutEffects_begin(subtreeRoot, root, committedLanes) {
  while (nextEffect !== null) {
    var fiber = nextEffect;
    var firstChild = fiber.child;
    //...
    if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
      // LayoutMask = Update | Callback | Ref | Visibility
      firstChild.return = fiber;
      nextEffect = firstChild;
    } else {
      commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
    }
  }
}
function commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes) {
  while (nextEffect !== null) {
    var fiber = nextEffect;
    if ((fiber.flags & LayoutMask) !== NoFlags) {
      var current = fiber.alternate;
      commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
    }
    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }
    var sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }
    nextEffect = fiber.return;
  }
}

主要逻辑还是封装在commitLayoutEffectOnFiber()中,也是根据不同的fiber.tag进行不同方法的调用

  • FunctionComponent: 触发commitHookEffectListMount()
  • ClassComponent: 先触发componentDidMount/componentDidUpdate,然后触发commitUpdateQueue()
  • HostRoot: 触发commitUpdateQueue()
  • HostComponent: 触发commitMount()
ts
function commitLayoutEffectOnFiber() {
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        if (!offscreenSubtreeWasHidden) {
          commitHookEffectListMount(Layout | HasEffect, finishedWork);
        }
        break;
      }
      case ClassComponent: {
        var instance = finishedWork.stateNode;
        if (finishedWork.flags & Update) {
          if (!offscreenSubtreeWasHidden) {
            if (current === null) {
              instance.componentDidMount();
            } else {
              var prevProps =
                finishedWork.elementType === finishedWork.type
                  ? current.memoizedProps
                  : resolveDefaultProps(
                      finishedWork.type,
                      current.memoizedProps
                    );
              var prevState = current.memoizedState; // We could update instance props and state here,
              instance.componentDidUpdate(
                prevProps,
                prevState,
                instance.__reactInternalSnapshotBeforeUpdate
              );
            }
          }
        }
        var updateQueue = finishedWork.updateQueue;
        if (updateQueue !== null) {
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }

        break;
      }

      case HostRoot: {
        var _updateQueue = finishedWork.updateQueue;

        if (_updateQueue !== null) {
          var _instance = null;

          if (finishedWork.child !== null) {
            switch (finishedWork.child.tag) {
              case HostComponent:
                _instance = getPublicInstance(finishedWork.child.stateNode);
                break;
              case ClassComponent:
                _instance = finishedWork.child.stateNode;
                break;
            }
          }
          commitUpdateQueue(finishedWork, _updateQueue, _instance);
        }
        break;
      }
      case HostComponent: {
        var _instance2 = finishedWork.stateNode;
        if (current === null && finishedWork.flags & Update) {
          var type = finishedWork.type;
          var props = finishedWork.memoizedProps;
          commitMount(_instance2, type, props);
        }
        break;
      }
      case HostText: {
        // We have no life-cycles associated with text.
        break;
      }
    }
  }
  if (!offscreenSubtreeWasHidden) {
    if (finishedWork.flags & Ref) {
      commitAttachRef(finishedWork);
    }
  }
}

2.1.1 commitHookEffectListMount()

ts
function commitHookEffectListMount(flags, finishedWork) {
  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        var create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

fiber.updateQueue取出当前fiber的hook单循环链表数据,不断遍历取出对应create方法执行,然后返回对应的destroy方法给hook.destory

create()就是下面useEffect(fn, [color])的fn,destroy就是返回的console.log("Effect destroyed")对应的方法

ts
useEffect(() => {
  return () => {
    console.log("Effect destroyed");
  };
}, [color]);

2.1.2 commitMount()

进行dom.focus()或者dom.src的赋值

ts
function commitMount(domElement, type, newProps, internalInstanceHandle) {
  switch (type) {
    case "button":
    case "input":
    case "select":
    case "textarea":
      if (newProps.autoFocus) {
        domElement.focus();
      }
      return;
    case "img": {
      if (newProps.src) {
        domElement.src = newProps.src;
      }
      return;
    }
  }
}

2.1.3 commitUpdateQueue()

我们从上面的分析可以知道,ClassComponent会先触发componentDidMount/componentDidUpdate,然后触发commitUpdateQueue()

我们可以构建一个如下的示例,当点击按钮触发this.setState()时会触发commitUpdateQueue(),如下面截图所示

js
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick = () => {
    // 触发 setState 并传入回调函数
    this.setState({ count: this.state.count + 1 }, () => {
      console.error("Effect.callback executed!", this.state.count);
    });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>Increment</button>
      </div>
    );
  }
}

Image

setState的回调什么时候放入到effect.callback中呢?

在下面的小节我们将简单分析下ClassComponent的setState流程

2.2 ClassComponent的setState()

从下面代码我们可以看出,setState()传入的回调放在了update.callback

ts
Component.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, "setState");
};
var classComponentUpdater = {
  isMounted: isMounted,
  enqueueSetState: function (inst, payload, callback) {
    var fiber = get(inst);
    var eventTime = requestEventTime();
    var lane = requestUpdateLane(fiber);
    var update = createUpdate(eventTime, lane);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      update.callback = callback;
    }

    var root = enqueueUpdate$1(fiber, update, lane);
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitions(root, fiber, lane);
    }
  },
};

那么update.callback是什么时候转移到fiber.updateQueue.callback中呢?

如下图所示,在beginWork()->case ClassComponent: 触发updateClassComponent()->processUpdateQueue()

processUpdateQueue()中,会将update放入到fiber.updateQueue.effects

Image

而在上面的Commit阶段中,我们会触发commitUpdateQueue(),如下面代码所示,也就是从fiber.updateQueue取出effects然后进行处理 Image

ts
function commitUpdateQueue(finishedWork, finishedQueue, instance) {
    // Commit the effects
    var effects = finishedQueue.effects;
    finishedQueue.effects = null;

    if (effects !== null) {
        for (var i = 0; i < effects.length; i++) {
            var effect = effects[i];
            var callback = effect.callback;

            if (callback !== null) {
                effect.callback = null;
                callCallback(callback, instance);
            }
        }
    }
}

至此,我们弄清楚了ClassComponent中如何触发LayoutEffect以及setState会触发什么流程的大体逻辑


3. useMemo()

useEffect()/useLayoutEffect()的逻辑基本一致

  • 获取当前fiberhook
  • 判断当前依赖是否发生了改变,如果已经发生了改变,则重新触发nextCreate()获取新的值存入hook.memoizedState
ts
function mountMemo(nextCreate, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
function updateMemo(nextCreate, deps) {
  var hook = updateWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      var prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  var nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

4. useCallback()

useEffect()/useLayoutEffect()的逻辑基本一致

  • 获取当前fiberhook
  • 判断当前依赖是否发生了改变,如果已经发生了改变,则更新当前传入的callback方法,否则返回上一次得到的callback方法
ts
function mountCallback(callback, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

function updateCallback(callback, deps) {
  var hook = updateWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      var prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

5. useRef()

useEffect()/useLayoutEffect()的逻辑更为简单

  • 获取当前fiberhook
  • 缓存传入的值,更新时直接返回缓存的值,而不是重新创建

改变 ref 不会触发重新渲染。这意味着 ref 是存储一些不影响组件视图输出信息的完美选择。例如,如果需要存储一个 interval ID 并在以后检索它,那么可以将它存储在 ref 中。只需要手动改变它的 current 属性 即可修改 ref 的值

使用 ref 可以确保:

  • 可以在重新渲染之间 存储信息(普通对象存储的值每次渲染都会重置)。
  • 改变它 不会触发重新渲染(状态变量会触发重新渲染)。
  • 对于组件的每个副本而言,这些信息都是本地的(外部变量则是共享的)
ts
function mountRef(initialValue) {
  var hook = mountWorkInProgressHook();
  var _ref2 = {
    current: initialValue,
  };
  hook.memoizedState = _ref2;
  return _ref2;
}
function updateRef(initialValue) {
  var hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

6. useContext()

使用Context的三种场景: FunctionComponent、ClassComponent、Context.Consumer


Context 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递

需要多个步骤:

  1. createContext()创建一个Context对象数据ThemeContext
  2. 在最外层包裹<Context.Provider value=>传递对应的数据
  3. 在某一个内层使用const { theme, toggleTheme } = useContext(ThemeContext)获取对应的Context数据,然后进行使用
  4. 部分使用场景,比如<Context.Consumer>可以直接获取对应的(Context数据)=>{}进行操作

这里可能有点疑问,都直接使用useContext(ThemeContext)获取对应的Context数据,为什么不直接就const {} = ThemeContext直接使用该数据呢?

那是因为ReactuseContext还进行了ThemeContext和当前fiber的额外处理,当ThemeContext变化时会触发当前fiber进行重新渲染...可能跟正常使用JS/使用Vue监听渲染相比较,这种做法会比较奇怪

ts
const ThemeContext = React.createContext({
    theme: 'light',
    toggleTheme: () => {}, // 占位函数
});
jsx
<ThemeContext.Provider value={{ theme, toggleTheme }}>
    <FunctionComponentDemo />
    <ClassComponentDemo />
    <ConsumerComponentDemo />
</ThemeContext.Provider>
ts
const { theme, toggleTheme } = useContext(ThemeContext);
jsx
<ThemeContext.Consumer>
    {({ theme, toggleTheme }) => (
        <div style={{ background: theme === 'dark' ? '#333' : '#fff', padding: '20px' }}>
            <h2>Context.Consumer Component</h2>
            <p>Current Theme: {theme}</p>
            <button onClick={toggleTheme}>Toggle Theme</button>
        </div>
    )}
</ThemeContext.Consumer>

接下来我们将针对上面4个步骤进行详细的分析

6.1 React.createContext()

创建一个context的对象数据,传入defaultValue赋值给_currentValue

ts
function createContext(defaultValue) {
  var context = {
    $$typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    _threadCount: 0,
    Provider: null,
    Consumer: null,
    _defaultValue: null,
    _globalName: null,
  };
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = context;

  return context;
}

6.2 <Context.Provider>

当使用 <Context.Provider> 时,JSX 会被 Babel 或 TypeScript 转换为 React.createElement 调用:

ts
// JSX: <Context.Provider value={value}>...</Context.Provider>
// 转换为:
React.createElement(Context.Provider, { value }, children);

生成的 React 元素(对象)结构为:

json
{
  $$typeof: Symbol(react.element),
  type: Context.Provider, // 关键属性
  props: { value, children },
  // ...其他元数据
}

createContext()可以知道,context.Provider为:

ts
Context.Provider = {
  $$typeof: REACT_PROVIDER_TYPE,
  _context: context,
};

因此生成的 React 元素(对象)结构实际为:

json
{
  $$typeof: Symbol(react.element),
  type: {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  },
  props: { value, children },
}

后续流程会创建fiber=>createFiberFromTypeAndProps()

由于type.$$typeof=REACT_PROVIDER_TYPE,从而命中fiberTag=ContextProvider,也就是tag=ContextProvider

ts
function createFiberFromTypeAndProps() {
  var fiberTag = IndeterminateComponent;
  var resolvedType = type;
  if (typeof type === "function") {
    //...
  } else if (typeof type === "string") {
    //...
  } else {
    getTag: switch (type) {
      //...
      default: {
        if (typeof type === "object" && type !== null) {
          switch (type.$$typeof) {
            case REACT_PROVIDER_TYPE:
              fiberTag = ContextProvider;
              break getTag;
            //...
          }
        }
      }
    }
  }
  var fiber = createFiber(fiberTag, pendingProps, key, mode);
  fiber.elementType = type;
  fiber.type = resolvedType;
  fiber.lanes = lanes;
  return fiber;
}

beginWork()命中ContextProvider => updateContextProvider()

tag=ContextProvider

ts
beginWork() {
  switch (workInProgress.tag) {
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
  }
}

updateContextProvider()的逻辑也比较简单:

  1. pushProvider()更新目前Context最新的值到栈中,并且更新context._currentValue的值
  2. 比较组件即将应用的新的props组件在上一次渲染中已经应用的props,如果值没有变化,则bailoutOnAlreadyFinishedWork()什么都不操作

bailoutOnAlreadyFinishedWork()一般返回null,这样代表当前fiber不需要更新,后续操作也不需要考虑该fiber

  1. 如果props.value已经改变,如下图所示,也就是创建createContext(value)value发生改变,则触发propagateContextChange()通知所有消费该Context的fiber进行更新渲染 Image

  2. 然后跟其它类型一样,正常触发reconcileChildren()进行diff,然后返回workInProgress.child

ts
function updateContextProvider(current, workInProgress, renderLanes) {
  var providerType = workInProgress.type;
  var context = providerType._context;
  var newProps = workInProgress.pendingProps;
  var oldProps = workInProgress.memoizedProps;
  var newValue = newProps.value;

  pushProvider(workInProgress, context, newValue);

  if (oldProps !== null) {
    var oldValue = oldProps.value;
    if (objectIs(oldValue, newValue)) {
      // No change. Bailout early if children are the same.
      if (oldProps.children === newProps.children && !hasContextChanged()) {
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes
        );
      }
    } else {
      // The context value changed. Search for matching consumers and schedule
      // them to update.
      propagateContextChange(workInProgress, context, renderLanes);
    }
  }

  var newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

我们是如何拿到目前Context最新的值的呢?providerType = workInProgress.type又是代表什么意思?

type就是createContext()创建的context.Provider,里面存放着各种数据,包括目前状态值,通过providerType._context就可以拿到当前的Context对象数据!

json
{
  $$typeof: Symbol(react.element),
  type: {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  },
  props: { value, children },
}

Image


接下来的小节中,我们将针对pushProvider()propagateContextChange()展开分析

6.2.1 pushProvider()

Context.Provider会存放在一个valueStack的栈中,并且这个栈有个特点

  • 最新一个数据会直接放在cursor.current
  • 倒数第二新的元素才会在栈顶上

比如:正常来说,valueCursor=[1, 2, 3, 4, 5]

但是目前的情况是

  • cursor.current=5
  • valueCursor=[1, 2, 3, 4]

当加入新的元素6时,会变成

  • cursor.current=6
  • valueCursor=[1, 2, 3, 4, 5]

而对于 <Context.Provider> ,多个不同的 <Context.Provider> 会使用同一个 valueCursor ,这个层级又加了一层,如下面代码所示:

  • 当前最新的 <Context.Provider> 的值放在 valueCursor.current 并且更新到 context._currentValue
  • 第二新的值移动到 valueStack 的栈中

初始化valueCursor = { current: null },每次调用 pushProvider() 更新目前Context最新的值到栈中,并且更新 valueCursor.currentcontext._currentValue的值

ts
var valueStack = [];
var index = -1;
var valueCursor = createCursor(null);
function createCursor(defaultValue) {
  return {
    current: defaultValue,
  };
}
function pushProvider(providerFiber, context, nextValue) {
  push(valueCursor, context._currentValue);
  context._currentValue = nextValue;
}
function push(cursor, value, fiber) {
  index++;
  valueStack[index] = cursor.current;
  cursor.current = value;
}

6.2.2 propagateContextChange()

由于涉及到useContext()注册fiber的相关监听,因此这里暂时不分析,放在6.3.2 propagateContextChange()展开分析

6.3 useContext()

useContext()实际的方法名称是readContext(),逻辑也比较简单:

  • 构建一个firstContext对象,具备Context这个对象、Context._currentValue这个对象的值、next(单链表结构), 然后赋值给当前的fiber.dependencies
  • 如果本身currentlyRenderingFiber.dependencies已经有值,则持续构建单链表数据即可(当前fiber可能有多个不同的Context)
ts
function readContext(context) {
  var value = context._currentValue;
  if (lastFullyObservedContext === context);
  else {
    var contextItem = {
      context: context,
      memoizedValue: value,
      next: null,
    };
    if (lastContextDependency === null) {
      lastContextDependency = contextItem;
      currentlyRenderingFiber.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
      };
    } else {
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return value;
}

上面代码中lastFullyObservedContextcurrentlyRenderingFiber又是在哪里赋值的呢?

这得说到一个方法,在多个渲染时触发,比如FunctionComponentrenderWithHooks()之前会触发prepareToReadContext()

在这个方法中,会进行lastFullyObservedContextcurrentlyRenderingFiber的赋值以及fiber.dependencies的清理

ts
function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) {
  prepareToReadContext(workInProgress, renderLanes);
  //...
  value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes);
  //...
}
function prepareToReadContext(workInProgress, renderLanes) {
  currentlyRenderingFiber = workInProgress;
  lastContextDependency = null;
  lastFullyObservedContext = null;
  var dependencies = workInProgress.dependencies;
  if (dependencies !== null) {
    var firstContext = dependencies.firstContext;
    if (firstContext !== null) {
      if (includesSomeLane(dependencies.lanes, renderLanes)) {
        // Context list has a pending update. Mark that this fiber performed work.
        markWorkInProgressReceivedUpdate();
      } // Reset the work-in-progress list

      dependencies.firstContext = null;
    }
  }
}

6.3.1 propagateContextChange()标记子fiber需要更新

分析完成useContext(),我们可以回到<Context.Provider>传入的值改变时触发的通知方法propagateContextChange()

下面的代码比较长,但是实际内容是比较简单的,本质就是一个深度遍历

从当前fiber.child开始,遍历它的fiber.dependencies(单链表,多个Context),看看是否注册了当前的Context数据

遍历完fiber.child,就遍历它的fiber.child.sibling,深度遍历所有的子fiber,检查对应的dependencies(单链表,多个Context)是否注册了当前的Context数据


fiber.dependencies中有注册了当前的Context数据

  • 通过mergeLanes(fiber.lanes, renderLanes)将当前renderLanes合并到当前fiber的lanes中
  • 通过scheduleContextWorkOnParentPath()将当前renderLanes不断向上传导更新到当前fiber的parentFiber的childLanes

父 Fiber 的 lanes 不为空但子 Fiber 的 lanes 为空时,父 Fiber 的更新可能会导致父组件重新渲染,而子组件可能被跳过不渲染

比如子 Fiber 的workInProgress.lanes & renderLanes === NoLanes =====> 子 Fiber 的优先级(lanes)与当前渲染批次(renderLanes)无交集


如果当前fiber是ClassComponent类型,则创建一个update

  • update.lane: renderLanes的最高优先级lane
  • update.tag: ForceUpdate

跟ClassComponent的setState()操作一样,setState()也会创建一个新的update,压入到队列中

??????但是setState()是如何触发更新的??

ts
function propagateContextChange(workInProgress, context, renderLanes) {
  propagateContextChange_eager(workInProgress, context, renderLanes);
}
function propagateContextChange_eager(workInProgress, context, renderLanes) {
  var fiber = workInProgress.child;
  if (fiber !== null) {
    fiber.return = workInProgress;
  }

  while (fiber !== null) {
    var nextFiber = void 0;

    var list = fiber.dependencies;

    if (list !== null) {
      nextFiber = fiber.child;
      var dependency = list.firstContext;

      while (dependency !== null) {
        if (dependency.context === context) {
          if (fiber.tag === ClassComponent) {
            var lane = pickArbitraryLane(renderLanes);
            var update = createUpdate(NoTimestamp, lane);
            update.tag = ForceUpdate;
            var updateQueue = fiber.updateQueue;
            if (updateQueue === null);
            else {
              var sharedQueue = updateQueue.shared;
              var pending = sharedQueue.pending;
              if (pending === null) {
                update.next = update;
              } else {
                update.next = pending.next;
                pending.next = update;
              }
              sharedQueue.pending = update;
            }
          }
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          var alternate = fiber.alternate;
          if (alternate !== null) {
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          scheduleContextWorkOnParentPath(
            fiber.return,
            renderLanes,
            workInProgress
          );
          list.lanes = mergeLanes(list.lanes, renderLanes);
          break;
        }
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
    } else if (fiber.tag === DehydratedFragment) {
      //...
    } else {
      nextFiber = fiber.child;
    }

    if (nextFiber !== null) {
      nextFiber.return = fiber;
    } else {
      nextFiber = fiber;
      while (nextFiber !== null) {
        if (nextFiber === workInProgress) {
          nextFiber = null;
          break;
        }
        var sibling = nextFiber.sibling;
        if (sibling !== null) {
          sibling.return = nextFiber.return;
          nextFiber = sibling;
          break;
        }
        nextFiber = nextFiber.return;
      }
    }

    fiber = nextFiber;
  }
}

6.3.2 子fiber触发更新

当我们使用下面的调试代码时,经过调试,我们可以知道,如果 FunctionComponentDemo 不使用 useContext(),那么即使父fiber改变了 theme,那么 FunctionComponentDemo 也不会触发更新

当使用 useContext() 后,父fiber改变了 themeFunctionComponentDemo 会触发更新

那么究竟是如何触发更新的呢?

ts
const ThemeContext = React.createContext('light');
const FunctionComponentDemo = React.memo(() => {
    const theme = useContext(ThemeContext);
    return <p>{theme}</p>;
});

function App() {
    const [theme, setTheme] = useState('light');
    const toggleTheme = () => {
        setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
    };
    return (
        <div>
            <ThemeContext.Provider value={theme}>
                <FunctionComponentDemo/>
            </ThemeContext.Provider>
            <button onClick={toggleTheme}>Toggle Theme</button>
        </div>

    )
}
const root = createRoot(document.getElementById('root'));
root.render(<App/>);

由上面 6.3.1 propagateContextChange()标记子fiber需要更新,我们可以知道,当使用 useContext() 后,父fiber改变了 theme,那么 FunctionComponentDemo 所在的 fiber.lanes就会被标记上更新lane !

在 父fiber改变了 theme -> 触发 父fiber执行 beginWork() 后,会触发 子fiber执行 beginWork(),从而执行 updateFunctionComponent()

updateFunctionComponent() 中,渲染更新流程如果 didReceiveUpdate = false,说明没有变化,则不继续执行 diff 和生成 childrenFibers 的流程,也就是不需要该 fiber 进行渲染更新!

ts
function updateFunctionComponent() {
  prepareToReadContext(workInProgress, renderLanes);

  nextChildren = renderWithHooks();

  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

  workInProgress.flags |= PerformedWork;
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}
function bailoutHooks(current, workInProgress, lanes) {
  workInProgress.updateQueue = current.updateQueue; 
  workInProgress.flags &= ~(Passive | Update);
  current.lanes = removeLanes(current.lanes, lanes);
}

但是在 prepareToReadContext(),我们会检测 fiber.dependencies 是否存在,并且 includesSomeLane(dependencies.lanes, renderLanes)

而这两个条件恰恰是 propagateContextChange() 所做的事情,因此会触发 markWorkInProgressReceivedUpdate() 设置 didReceiveUpdate = true,从而继续向下执行 diff 和生成 childrenFibers 的流程,触发更新!

ts
function prepareToReadContext(workInProgress, renderLanes) {
  currentlyRenderingFiber = workInProgress;
  var dependencies = workInProgress.dependencies;
  if (dependencies !== null) {
    var firstContext = dependencies.firstContext;
    if (firstContext !== null) {
      if (includesSomeLane(dependencies.lanes, renderLanes)) {
        markWorkInProgressReceivedUpdate();
      }
      dependencies.firstContext = null;
    }
  }
}

6.4 </Context.Provider>

completeWork进行popProvider()

6.5 <Context.Consumer>

createContext()可以知道,updateContextConsumer()拿到的type就是context

这里省略了根据$$typeof: REACT_CONTEXT_TYPE设置对应的tag=ContextConsumer的代码展示

ts
function createContext(defaultValue) {
  var context = {
    $$typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    _threadCount: 0,
    Provider: null,
    Consumer: null,
    _defaultValue: null,
    _globalName: null,
  };
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = context;
  return context;
}
beginWork() {
  switch (workInProgress.tag) {
    case ContextConsumer:
        return updateContextConsumer(current, workInProgress, renderLanes);
  }
}
function updateContextConsumer(current, workInProgress, renderLanes) {
  var context = workInProgress.type;
  var newProps = workInProgress.pendingProps;
  var render = newProps.children;
  prepareToReadContext(workInProgress, renderLanes);
  var newValue = readContext(context);
  var newChildren = render(newValue);
  workInProgress.flags |= PerformedWork;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

updateContextConsumer()中,我们触发

  • prepareToReadContext(): 清理fiber.dependencies以及其它属性
  • readContext(): 进行Context与当前fiber的绑定!
    • 构建一个firstContext对象,具备Context这个对象、Context._currentValue这个对象的值、next(单链表结构), 然后赋值给当前的fiber.dependencies
    • 如果本身currentlyRenderingFiber.dependencies已经有值,则持续构建单链表数据即可(当前fiber可能有多个不同的Context)
  • newChildren = render(newValue): 本质就是<ThemeContext.Consumer>包裹的渲染函数,如下面的代码所示,包裹的渲染函数传入context对象={ theme, toggleTheme }
  • 跟其它fiber一样,触发reconcileChildren()进行diff,然后返回workInProgress.child
jsx
function ConsumerComponentDemo() {
    return (
        <ThemeContext.Consumer>
            {({ theme, toggleTheme }) => (
                <div style={{ background: theme === 'dark' ? '#333' : '#fff', padding: '20px' }}>
                    <h2>Context.Consumer Component</h2>
                    <p>Current Theme: {theme}</p>
                    <button onClick={toggleTheme}>Toggle Theme</button>
                </div>
            )}
        </ThemeContext.Consumer>
    );
}
ts
render = function (_ref) {
  var theme = _ref.theme,
    toggleTheme = _ref.toggleTheme;
  return React.createElement(
    "div",
    {
      style: {
        background: theme === "dark" ? "#333" : "#fff",
        padding: "20px",
      },
    },
    React.createElement("h2", null, "Context.Consumer Component"),
    React.createElement("p", null, "Current Theme: ", theme),
    React.createElement("button", { onClick: toggleTheme }, "Toggle Theme")
  );
};

6.6 多个Context包裹的情况

场景行为
同类型 Context 嵌套子组件获取最近的 Provider 的值,内层覆盖外层
不同类型 Context 嵌套各 Context 独立,子组件可同时消费多个 Context 的值
Provider 未设置 value使用 createContext 的默认值,并抛出警告
外层 Provider 的 value 变化不影响已嵌套在内层 Provider 的子组件(除非子组件依赖外层 Context)

面对多个Context包裹的情况,比如下面代码所示,代码中是如何做到区分当前最近的Context的呢?

jsx
const ThemeContext = React.createContext("light");

function App() {
  return (
          <ThemeContext.Provider value="dark">
            <Parent>
              <ThemeContext.Provider value="blue">
                <Child />
              </ThemeContext.Provider>
            </Parent>
          </ThemeContext.Provider>
  );
}

对于不同的Context呢?比如下面代码所示,代码中是如何做到区分当前最近的Context的呢?

jsx
const ThemeContext = React.createContext("light");

function App() {
  return (
          <ThemeContext.Provider value="dark">
            <Parent>
              <MyContext.Provider value="blue">
                <Child />
              </MyContext.Provider>
            </Parent>
          </ThemeContext.Provider>
  );
}

区分多个嵌套Context使用的是valueStack,valueStack 的作用是 管理嵌套的 Context Provider 的值,确保在组件树渲染过程中,每个层级的 Provider 能正确覆盖或恢复其关联的 Context 值

  • 具体来说,当进入一个 Provider 组件时,React 会将当前 Context 的值压入栈(valueStack),然后更新为新的值
  • 当退出该 Provider 的作用域时,再从栈中弹出旧值,恢复之前的 Context 值

不同的 Context 会共享同一个全局的 valueStack,但通过 栈的先进后出(FILO)特性 和 与当前处理的 Context 对象绑定,确保每个 Context 的值在嵌套层级中正确隔离和恢复

核心机制为:

  • 全局的 valueStack:所有类型的 Context 在更新值时,共享同一个全局的 valueStack 数组。
  • 操作顺序:每个 Context 的旧值在进入其 Provider 时被压入栈,退出时弹出恢复,严格遵循 嵌套顺序 => 通过beginWorkcompleteWork()的执行顺序
  • 上下文绑定:valueStack 仅存储值,而 值的归属(属于哪个 Context) 由代码执行顺序隐式保证

注:传入的newValue是目前 <FirstContext.Provider="newValue"> 传入的 newValue,可能有多个 <FirstContext.Provider="xxx"> 会不断更新 context._currentValue

并且我们要关注初始化时 createContext(defaultValue) 会进行 context._currentValue = defaultValue 的赋值!

多个不同的 Context 共用同一个 valueStack,比如 <FirstContext.Provider="newValue">,然后 <SecondContext.Provider="newValue"> 都会压入值到 valueStack + valueCursor

ts
const valueStack = [];
function pushProvider(context, newValue) {
  var value = context._currentValue;
  // 压入栈
  index++;
  valueStack[index] = cursor.current;
  cursor.current = value;
  // 更新context的值
  context._currentValue = newValue; // 下一次popProvider()作为valueCursor的值
}

function popProvider(context) {
  // 更新context的值
  var currentValue = valueCursor.current;
  if (currentValue === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED) {
    context._currentValue = context._defaultValue;
  } else {
    context._currentValue = currentValue;
  }
  // 弹出栈
  if (index < 0) {
    return;
  }
  cursor.current = valueStack[index]; // 下一次popProvider()作为valueCursor的值
  valueStack[index] = null;
  index--;
}

6.5.1 具体示例

jsx
const ThemeContext = React.createContext("light");
const UserContext = React.createContext("guest");

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <UserContext.Provider value="user">
        <Child />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

function Child() {
  const theme = useContext(ThemeContext); // "dark"
  const user = useContext(UserContext);   // "user"
  return <div>{theme} - {user}</div>;
}
步骤操作栈内容(从底到顶)Context 当前值
进入ThemeContext.ProviderpushProvider(ThemeContext, "dark")["light"]ThemeContext._currentValue = "dark"
进入UserContext.ProviderpushProvider(UserContext, "user")["light", "guest"]UserContext._currentValue = "user"
渲染Child消费两个 Context 的值栈保持不变 ThemeContext: "dark", UserContext: "user"
退出UserContext.ProviderpopProvider(UserContext)["light"]UserContext._currentValue = "guest"
退出ThemeContext.ProviderpopProvider(ThemeContext)[]ThemeContext._currentValue = "light"