React 上下文管理游标和栈的设计原理

200 阅读7分钟

著有《React 源码》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。

文章不好写,要是有帮助别忘了点赞,收藏,评论 ~你的鼓励是我继续挖干货的动力🔥。

另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~

概念和组成

“游标”是指向栈中元素的一个指针,例如指向了栈顶元素。
这里介绍 2个公共的栈 + 5个游标。

// 声明、初始化
var valueStack = [];//1.包含上下文Context、上下文Context是否change、fiber、嵌套信息...混合了所有的游标类型
var fiberStack; //2.只有fiber类型

var contextStackCursor = createCursor(emptyContextObject);
var didPerformWorkStackCursor = createCursor(false);


var contextStackCursor$1 = createCursor(NO_CONTEXT);
var contextFiberStackCursor = createCursor(NO_CONTEXT);
var rootInstanceStackCursor = createCursor(NO_CONTEXT);
//初始化默认值
var emptyContextObject = {};
var previousContext = emptyContextObject;

var NO_CONTEXT = {};

{
  fiberStack = [];//初始化fiberStack
}
var index = -1;

function createCursor(defaultValue) {
  return {
    current: defaultValue
  };
}

游标和栈示例

游标分类

  1. 旧版的Context API用到的游标
  • contextStackCursor 把Context作为上下文对象
  • didPerformWorkStackCursor 把Context是否变化了作为上下文对象
  1. DOM节点的嵌套信息 和 这个DOM节点对应的Fiber实例
  • contextFiberStackCursor,是和contextStackCursor$1 一起用的,是当前的fiber节点
  • contextStackCursor$1,把DOM节点的嵌套信息作为上下文。这个游标的值是nextContext。在updatedAncestorInfo计算当前html标签的ancestorInfo
push(contextStackCursor$1, nextContext, fiber);

//祖先嵌套信息
nextContext = {
  ancestorInfo:{
    aTagInScope: null
    buttonTagInScope : null
    current: {tag: 'div'}
    dlItemTagAutoclosing: null
    formTag: null
    listItemTagAutoclosing: null
    nobrTagInScope: null
    pTagInButtonScope: null
  }
  namespace: "http://www.w3.org/1999/xhtml"
}

`current`不能跨级,每一次都是新的,表示当前html标签的直接父标签。
`pTagInButtonScope`...等`可以跨级`,表示当前标签的祖先是否有p标签,一直等到当前节点也是`p`等才更新为当前`p`
  1. HostRoot和HostPortal类型的Fiber节点,它们的容器DOM节点
  • rootInstanceStackCursor,HostRoot和HostPortal类型的Fiber节点,所对应的DOM节点对象。这表明是根容器,如div#root。可以有多层嵌套的HostRoot HostPortal。比如:
- HostRoot (div#root)
    └─ HostPortal (div#a)
        └─ HostPortal (div#b)
            └─ HostPortal (div#c)

例:ReactDOM.createPortal(, document.getElementById("a"))

入栈函数分类 2 + 1(没有)+ 2(综合)

  1. 旧版的Context API用到的游标
  • pushContextProvider 设置contextStackCursor和didPerformWorkStackCursor
  • invalidateContextProvider 设置contextStackCursor和didPerformWorkStackCursor
  • pushTopLevelContextObject 设置HostRoot的contextStackCursor和didPerformWorkStackCursor
  • 场景:tag=HostRoot类型的节点,pushTopLevelContextObject。
    旧版的类组件、使用了Context的场景使用,pushContextProvider和invalidateContextProvider联合使用。
    pushContextProvider是为了尽早push,放入的是上一次旧值,invalidateContextProvider会先把旧的拿出来,放入这一轮新的。
    这一轮先放入了上一轮旧的,尽早放入值,确保堆栈的深度、长度、层级是对的,但是值可能是过期的。
  1. DOM嵌套信息和这个DOM对应Fiber
  • pushHostContext设置contextStackCursor$1和contextFiberStackCursor。
  • 场景:HostComponent类型的Fiber节点使用
  1. HostRoot和HostPortal类型的Fiber节点,它们的容器DOM节点
  • ❌ 没有独立的设置rootInstanceStackCursor游标的函数
  1. 综合设置游标-设置3个游标
  • pushHostContainer 设置rootInstanceStackCursor 和 contextFiberStackCursor和contextStackCursor$1。
  • 场景:HostPortal 、pushHostRootContext(),需要综合设置3个游标,HostRoot需要设置5个游标。
  1. 设置全部游标
  • pushHostRootContext
  • 场景:HostRoot类型的Fiber节点需要一次性设置全部游标

源码 6个入栈函数

  1. 旧版的Context API用到的游标 contextStackCursor和didPerformWorkStackCursor
var emptyContextObject = {};
function pushContextProvider(workInProgress) {
  {
    var instance = workInProgress.stateNode; 
    
    // ‼️看注释1/2: push early   stack integrity
    // 当前层,pushContextProvider()专门放入旧值(上一层的值),为了“early as possible”。
    // invalidateContextProvider()会先pop取出旧值,正式放入当前层的值。
    // pushContextProvider和invalidateContextProvider必须成对使用
    
    // We push the context as early as possible to ensure stack integrity. 
    // If the instance does not exist yet, we will push null at first, 
    // and replace it on the stack later when invalidating the context.
    var memoizedMergedChildContext = instance && instance.__reactInternalMemoizedMergedChildContext || emptyContextObject; 

    previousContext = contextStackCursor.current;
    push(contextStackCursor, memoizedMergedChildContext, workInProgress);
    push(didPerformWorkStackCursor, didPerformWorkStackCursor.current, workInProgress);
    return true;
  }
}
function invalidateContextProvider(workInProgress, type, didChange) {
  {
    var instance = workInProgress.stateNode;

    if (!instance) {
      throw new Error('Expected to have an instance by this point. ' + 'This error is likely caused by a bug in React. Please file an issue.');
    }

    if (didChange) {
      // 合并上下文对象,类似于assign({},obj1,obj2}
      var mergedContext = processChildContext(workInProgress, type, previousContext);
      instance.__reactInternalMemoizedMergedChildContext = mergedContext; 
      // ‼️看注释2/2:old or empyt, pushContextProvider的是empty或者这里合并的旧值,先把他们取出来
      // Replace the old (or empty) context with the new one. 
      // It is important to unwind the context in the reverse order.
      pop(didPerformWorkStackCursor, workInProgress);
      pop(contextStackCursor, workInProgress); 

      push(contextStackCursor, mergedContext, workInProgress);
      push(didPerformWorkStackCursor, didChange, workInProgress);
    } else {
      pop(didPerformWorkStackCursor, workInProgress);
      push(didPerformWorkStackCursor, didChange, workInProgress);
    }
  }
}
function pushTopLevelContextObject(fiber, context, didChange) {
  {
    if (contextStackCursor.current !== emptyContextObject) {
      throw new Error('Unexpected context found on stack. ' + 'This error is likely caused by a bug in React. Please file an issue.');
    }

    push(contextStackCursor, context, fiber);
    push(didPerformWorkStackCursor, didChange, fiber);
  }
}
  1. DOM嵌套信息和这个DOM对应Fiber contextStackCursor$1和contextFiberStackCursor
function pushHostContext(fiber) {
  var rootInstance = requiredContext(rootInstanceStackCursor.current);
  var context = requiredContext(contextStackCursor$1.current);
  var nextContext = getChildHostContext(context, fiber.type); 

  // Don't push this Fiber's context unless it's unique.
  if (context === nextContext) {
    return;
  } 

  push(contextFiberStackCursor, fiber, fiber);
  push(contextStackCursor$1, nextContext, fiber);
}
  1. tag=HostPortal 、pushHostRootContext() 需要综合设置3个游标
function pushHostContainer(fiber, nextRootInstance) {
  push(rootInstanceStackCursor, nextRootInstance, fiber); 

  push(contextFiberStackCursor, fiber, fiber); 

  push(contextStackCursor$1, NO_CONTEXT, fiber);
  var nextRootContext = getRootHostContext(nextRootInstance); 

  pop(contextStackCursor$1, fiber);
  push(contextStackCursor$1, nextRootContext, fiber);
}
  1. tag=HostRoot设置全部5个游标,rootInstanceStackCursor...(其他cursor)
function pushHostRootContext(workInProgress) {
  var root = workInProgress.stateNode;

  if (root.pendingContext) {
    pushTopLevelContextObject(workInProgress, root.pendingContext, root.pendingContext !== root.context);
  } else if (root.context) {
    pushTopLevelContextObject(workInProgress, root.context, false);
  }

  pushHostContainer(workInProgress, root.containerInfo);
}

为什么要栈、为什么要游标

因为处在现在的值时需要知道上一个值。

  1. 旧版Context的嵌套,按照正确的顺序获取Context
  2. DOM嵌套,校验html嵌套是否合法
  3. 容器嵌套,...

对应游标

  1. 最新的Context的值
  2. 当前层级DOM节点的嵌套信息
  3. 当前的容器DOM节点,例如div#root、div#modal-root
ReactDOM.createPortal(<Modal />, document.getElementById("modal-root"))
  • 例如:
A(Provider)
 └─ B(Provider)
     └─ C(Consumer)
            valueStack           contextStackCursor.current
A push       {}                  {}+A
B push       {}+A                {}+A+B
C render     {}+A                {}+A+B   ← 与 B 相同

渲染到C的时候需要知道Context的嵌套情况,当前的{}+A+B,和上一个{}+A。

注意Consumer是消费上下文,C组件没有自己的Context,所以没有push C Context。

应该和B保持一样,因为C没有push

  • 例如:React completeWork归阶段需要校验DOM嵌套是否合法。(validateDOMNesting
function getHostContext() {
  var context = requiredContext(contextStackCursor$1.current);
  return context;
}

var currentHostContext = getHostContext();//contextStackCursor$1.current

var hostContextDev = hostContext;
//validateDOMNesting需要校验html嵌套是否合法,validateDOMNesting第三个参数
var ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type);
validateDOMNesting(null, string, ownAncestorInfo);

push和参数

cursor分别是contextStackCursor、didPerformWorkStackCursor...5个游标。
value是nextContext fiber didChange等,如祖先信息对象、Provider-Context上下文对象。
我们在push的时候提供的第二个参数value是第一个参数游标的最新的值。

function push(cursor, value, fiber) {
  index++;
  valueStack[index] = cursor.current;//旧的 valueStack是栈,栈顶是上一层的

  {
    fiberStack[index] = fiber;
  }

  cursor.current = value;//最新的  cursor.current是游标,是最新的当前层的
}
用例:
push(contextStackCursor, context, fiber);//context是contextStackCursor的值
push(didPerformWorkStackCursor, didChange, fiber);//cursor是didPerformWorkStackCursor,value是didChange
  • valueStack是栈,栈顶是上一层的
  • cursor.current是游标,是最新的当前层的,但在例子中,可能是当前层的context也可能是当前层的didChange,最新的值是当前层的didChange
  • fiberStack是栈,栈顶是当前fiber,用于在pop时比对是否成对的进行push和pop。
    因为一次入栈函数内部会多次push,所以会把相同的fiber重复放入fiberStack,这样虽然valueStack不是同样的值,但是都是同样的fiber上进行的valueStack(一对多,同一个fiber多个valueStack),所以pop可以比对fiber和fiberStack的值来确定push和pop是否成对。

valueStack混合了所有游标类型的值

valueStack看起来非常的“乱”,混合了所有游标类型的值,不能预测里面的值,无法仅从 valueStack[i] 知道它属于哪个上下文,但是push/pop总是成对出现的。

栈和成对出现的push/pop,正确的维护了游标的值,出栈的时候更新游标为上一个上下文。
这样处在当前位置,可以直接从游标取到当前位置的上下文。

function pop(cursor, fiber) {
  //栈空了
  if (index < 0) {
    {
      error('Unexpected pop.');
    }
    return;
  }
  {
    //通过验证 游标对应fiber和fiberStack 是不是对应,确认push和pop是成对的
    if (fiber !== fiberStack[index]) {
      error('Unexpected Fiber popped.');
    }
  }
  //valueStack游标出栈
  cursor.current = valueStack[index]; //更新为上一层上下文。更新为正确的游标。
  valueStack[index] = null;
  {
    //faberStack游标出栈
    fiberStack[index] = null;
  }
  index--;
}

用例

截屏2025-12-11 下午4.25.25.png 因为只有这几种游标,所以对于简单的场景,我们可以直接知道结果:
0到4是顶级一次综合设置的5个游标 (是固定的)。
5和6是一次入栈函数两次push的(因为5 6的fiberStack是相同的),我没有用到上下文,所以是嵌套。
7是只有一个push的(因为7的fiberStack是不同的),后期还留意到subtreeRenderLanescursor游标,正是它。(后期还留意到subtreeRenderLanescursor、suspenseStackCursor游标,都是放到valueStack栈,没有继续调试。)

关注每个函数push了几次,pop和push能相互抵消,计算的时候注意抵消掉。
push都是放入valueStack[index],通过push次数可以知道valueStack一次增加了几个值,这样看valueStack变量就可以大致推算调用了什么入栈函数,然后推算做了嵌套、上下文、还是什么。

function pushRenderLanes(fiber, lanes) {
  push(subtreeRenderLanesCursor, subtreeRenderLanes, fiber);
  subtreeRenderLanes = mergeLanes(subtreeRenderLanes, lanes);
  workInProgressRootIncludedLanes = mergeLanes(workInProgressRootIncludedLanes, lanes);
}
function popRenderLanes(fiber) {
  subtreeRenderLanes = subtreeRenderLanesCursor.current;
  pop(subtreeRenderLanesCursor, fiber);
}

function pushSuspenseContext(fiber, newContext) {
  push(suspenseStackCursor, newContext, fiber);
}
function popSuspenseContext(fiber) {
  pop(suspenseStackCursor, fiber);
}

其他源码

updateClassComponent-pushContextProvider、finishClassComponent-invalidateContextProvider一起使用

function isContextProvider(type) {
  {
    var childContextTypes = type.childContextTypes;
    return childContextTypes !== null && childContextTypes !== undefined;
  }
}
function updateClassComponent(current, workInProgress, Component, nextProps, renderLanes) {
  var hasContext;
   //类组件,且组件中有childContextTypes属性,才会入栈,设置游标,contextStackCursor是类组件的游标
  if (isContextProvider(Component)) {
    hasContext = true;
    pushContextProvider(workInProgress);
  } else {
    hasContext = false;
  }
}
function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) {

  var didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;

  //既没有更新,也没有错误,跳过后续渲染
  if (!shouldUpdate && !didCaptureError) {
    // Context providers should defer to sCU for rendering
    if (hasContext) { //就是上面设置的hasContext
      invalidateContextProvider(workInProgress, Component, false);
    }

    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  
 //有更新
  ...
 if (hasContext) {//就是上面设置的hasContext
    invalidateContextProvider(workInProgress, Component, true);
  }
 return workInProgress.child;
}

总结

只要知道游标和栈的核心用法:push和pop成对出现,栈保存上一个的值,游标保存最新的值,pop更新游标的值,使用游标就能得到正确的值。到了各种具体的使用场景都是一样的:不论是5种游标还是6、7种游标。