React 上下文游标栈 contextStackCursor valueStack fiberStack

在beginWork completeWork的过程中switch catch到updateHostComponent就有一行代码

completeWork是对应的popHostContext。

总是在fiber节点的beginWork/completeWork,在updateHostComponent等的第一行出现。

概念和组成

"游标"是指向栈中元素的一个指针,例如指向了栈顶元素。

游标栈是 栈 + 游标。

2个公共的栈 + 5个游标组成

js 复制代码
var valueStack = [];//1.包含3个内容:包含Provider的Context是否change、上下文Context、fiber
var fiberStack; //2.只包含fiber

{
  fiberStack = [];
}

var index = -1;

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


// 3.Provider提供的Context的游标
var contextStackCursor = createCursor(emptyContextObject);
// 4.对应的Context是否change的游标
var didPerformWorkStackCursor = createCursor(false);

// 上一个contextStackCursor,用于新旧contextStackCursor的合并
var previousContext = emptyContextObject;


var NO_CONTEXT = {};
// 5.实际dom元素的fiber节点的上下文游标
var contextStackCursor$1 = createCursor(NO_CONTEXT);
// 6.fiber节点的游标
var contextFiberStackCursor = createCursor(NO_CONTEXT);
// 7.根节点HostRoot游标
var rootInstanceStackCursor = createCursor(NO_CONTEXT);

contextStackCursor$1是最新的HostComponent的游标

contextStackCursor是最新的ClassComponent且设置了childContextTypes属性的类组件的游标

5个入栈函数

  1. 设置contextStackCursor和didPerformWorkStackCursor游标
js 复制代码
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. 设置contextStackCursor和didPerformWorkStackCursor游标
js 复制代码
var emptyContextObject = {};
function pushContextProvider(workInProgress) {
  {
    var instance = workInProgress.stateNode; 
    
    // 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; 
    // Remember the parent context so we can merge with it later.
    // Inherit the parent's did-perform-work value to avoid inadvertently blocking updates.

    previousContext = contextStackCursor.current;
    push(contextStackCursor, memoizedMergedChildContext, workInProgress);
    push(didPerformWorkStackCursor, didPerformWorkStackCursor.current, workInProgress);
    return true;
  }
}
  1. 设置contextFiberStackCursor contextStackCursor$1 和rootInstanceStackCursor 游标
js 复制代码
function pushHostContainer(fiber, nextRootInstance) {
  // Push current root instance onto the stack;
  // This allows us to reset root when portals are popped.
  push(rootInstanceStackCursor, nextRootInstance, fiber); 
  
  // Track the context and the Fiber that provided it.
  // This enables us to pop only Fibers that provide unique contexts.
  push(contextFiberStackCursor, fiber, fiber); 
  
  // Finally, we need to push the host context to the stack.
  // However, we can't just call getRootHostContext() and push it because
  // we'd have a different number of entries on the stack depending on
  // whether getRootHostContext() throws somewhere in renderer code or not.
  // So we push an empty value first. This lets us safely unwind on errors.
  push(contextStackCursor$1, NO_CONTEXT, fiber);
  var nextRootContext = getRootHostContext(nextRootInstance); 
  
  // Now that we know this function doesn't throw, replace it.
  pop(contextStackCursor$1, fiber);
  push(contextStackCursor$1, nextRootContext, fiber);
}
  1. 设置contextFiberStackCursor contextStackCursor$1游标
js 复制代码
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;
  } 
  
  // Track the context and the Fiber that provided it.
  // This enables us to pop only Fibers that provide unique contexts.
  push(contextFiberStackCursor, fiber, fiber);
  push(contextStackCursor$1, nextContext, fiber);
}
  1. 设置contextStackCursor和didPerformWorkStackCursor游标 合并上下文,设置到contextStackCursor
js 复制代码
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) {
      // Merge parent and own context.
      // Skip this if we're not updating due to sCU.
      // This avoids unnecessarily recomputing memoized values.
      // 合并上下文对象,类似于assign({},obj1,obj2}
      var mergedContext = processChildContext(workInProgress, type, previousContext);
      instance.__reactInternalMemoizedMergedChildContext = mergedContext; 
      // Replace the old (or empty) context with the new one.
      // It is important to unwind the context in the reverse order.
      // 先把pushContextProvider的取出来
      pop(didPerformWorkStackCursor, workInProgress);
      pop(contextStackCursor, workInProgress); 
      
      // Now push the new context and mark that it has changed.
      push(contextStackCursor, mergedContext, workInProgress);
      push(didPerformWorkStackCursor, didChange, workInProgress);
    } else {
      pop(didPerformWorkStackCursor, workInProgress);
      push(didPerformWorkStackCursor, didChange, workInProgress);
    }
  }
}

其中三个都是设置contextStackCursor和didPerformWorkStackCursor游标,下面具体介绍。

这5个入栈函数是在不同的fiber节点tag类型使用的:

  1. beginWork:HostRoot-updateHostRoot-pushHostRootContext-pushTopLevelContextObjectpushHostContainer

  2. beginWork:ClassComponent-updateClassComponent-pushContextProvider isContextProvider hasContext = true

    beginWork:ClassComponent-updateClassComponent-mountClassInstance-getUnmaskedContextgetMaskedContext

    beginWork:ClassComponent-updateClassComponent-finishClassComponent-invalidateContextProvider hasContext

  3. beginWork:HostComponent-updateHostComponent-pushHostContext

介绍

1.pushHostRootContext内部调用pushTopLevelContextObjectpushHostContainer把5个游标都设置,用于HostRoot节点,也就是差不多是div#rootfiber

pushContextProvider 只设置contextStackCursor didPerformWorkStackCursor游标。它是在类组件(ClassComponent)、且有childContextTypes属性的情况下调用。有childContextTypes表明它是一个上下文提供者Provider。在最后面的代码中有例子。

pushHostContext设置contextFiberStackCursor contextStackCursor$1游标。在dom实例对应的fiber节点,fiberNode.tag=5 即HostComponent(dom元素对应的fiber)的情况使用。contextStackCursor$1保存当前fiber的上下文。

上下文对象:一个是我们在validateDOMNesting校验dom嵌套结构介绍过的保存祖先信息的对象,还有一个是Provider-Context上下文对象

js 复制代码
//祖先嵌套信息
{
  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"
}
  1. getUnmaskedContextgetMaskedContext

Unmasked : unmasked 意为"未被遮盖的"。在编程中,mask(掩码)是一个非常重要的概念,它是一个二进制值 ,用于通过位运算来选择性地保留或隐藏另一个值中的某些位元。unmasked 则表示一个值在被获取之前,没有经过位运算的过滤或修改,它是其原始形式。

对应的是Provider-Context的contextTypes属性。

上上下下的Provider-Context合并起来,供全局使用,类似于assign({},obj1,obj2}。合并后的是UnmaskedContext,没有过滤的。过滤出要使用的是,过滤后的是MaskedContext的,依据的就是contextTypes属性进行过滤。在源码的getUnmaskedContextgetMaskedContext进行。

  1. isContextProvider hasContext = true invalidateContextProvider

isContextProvider函数判断类组件是否有childContextTypes属性。然后把hasContext设置为true/false。

childContextTypescontextTypes是旧版的Provider-Context的用法。

childContextTypes属性,就把上上下下的Context合并起来,对应源码invalidateContextProvider

有这样的属性的类组件,

js 复制代码
// 提供context
// ...一些代码
// 提供了childContextTypes {theme:'dark',other:...}

// 消费 context
class ThemedButton extends React.Component {
  // 声明需要订阅哪些 context
  static contextTypes = {// getMaskedContext内部获取这个属性
    theme: PropTypes.string, //只要theme
  };

  render() {
    return <button>当前主题: {this.context.theme}</button>;
  }
}
  1. invalidateContextProviderpushContextProvider

pushContextProviderinvalidateContextProvider联合使用,除了HostRoot后续fiber都是在isContextProvider为true下使用。

挂载阶段instance是null,memoizedMergedChildContext=emptyContextObject为了尽早push,使用pushContextProvide

如果上下文变了,invalidateContextProviderpushContextProviderpush的pop,然后push新的。

我们看一看顺序:

a. updateClassComponent

b. 挂载阶段,实例还没创建pushContextProvider

c. 创建实例constructClassInstance:var instance = new ctor(props, context);

d. finishClassComponent

e. invalidateContextProvider合并上下文

更新阶段instance不是null,也是尽早push,使用缓存的合并值,在finishClassComponent时发现变了,先把push的pop,最后push新的。

js 复制代码
function updateClassComponent(current, workInProgress, Component, nextProps, renderLanes) {
  ...
  var hasContext;

  if (isContextProvider(Component)) {
    hasContext = true;
    pushContextProvider(workInProgress);
  } else {
    hasContext = false;
  }
  ...
  var nextUnitOfWork = finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes);
  
}
js 复制代码
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) {
      invalidateContextProvider(workInProgress, Component, false);
    }

    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  
  ...
 if (hasContext) {
    invalidateContextProvider(workInProgress, Component, true);
  }
 return workInProgress.child;
}

这么复杂设计的游标栈在哪里使用

1.completeWork的dom实例创建(createInstance)和更新(updateHostComponent$1),文本节点实例的创建(createTextInstance)。

只有这两个涉及到dom的需要检查html标签嵌套的检查,会用到游标栈保存的上写文,对于其他类型的fiber,都不会用到游标栈。

completeWork:HostComponent-updateHostComponent$1-prepareUpdate更新dom实例

completeWork:HostComponent-createInstance 创建dom实例

completeWork:HostText-createTextInstance 创建文本dom实例

这些都validateDOMNesting检查嵌套,需要祖先标签信息,祖先信息保存在上下文。

源码completeWork createInstance createTextInstance

为什么dom实例更新也要检查嵌套?

举个例子:

jsx 复制代码
function App({ bad }) {
  return <table>{bad ? <div>oops</div> : <tr><td>ok</td></tr>}</table>;
}

a. 初次渲染 bad = false,生成 <table><tr><td>ok</td></tr></table> ✅ 合法。

b. 更新时 bad = true,React diff 发现 <tr><div>,于是准备在 同一个 table DOM 节点 里插入 <div>

c. 这时候如果不在 update 里跑 validateDOMNesting,React 就不会报错,最终会出现 <table><div>oops</div></table> ❌ 不合法的 DOM。

2.函数组件渲染的时候,传入函数组件的内部,函数组件的第二个参数

updateSimpleMemoComponent-updateFunctionComponent-renderWithHooks

mountLazyComponent:FunctionComponent-updateFunctionComponent-renderWithHooks

beginWork:FunctionComponent-updateFunctionComponent-renderWithHooks

这些情况都Component(props, secondArg);

jsx 复制代码
function updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes) {
  ... 
  var context;

  {
    var unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
    context = getMaskedContext(workInProgress, unmaskedContext);
  }

  ...
  nextChildren = renderWithHooks(current, workInProgress, Component, nextProps, context, renderLanes);
  ...
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
  ...
  var children = Component(props, secondArg); // Check if there was a render phase update
  ...
  return children;
}
js 复制代码
以前(React <16.3)函数组件是可以写成这样:
function MyComponent(props, context) {
  console.log(props, context); 
  return <div>{context.theme}</div>;
}

MyComponent.contextTypes = {
  theme: PropTypes.string
};

为什么不把上下文保存在fiber节点上,而是通过栈+游标?

我们需要跨级取用,contextStackCursor$1是最新的HostComponent的游标,只会看到dom元素的fiber。

div-CustomComponent-p,这样的嵌套结构,我们不需要CustomComponent,我们需要的是div-p,contextStackCursor$1是div的,到p进入pushHostContext,contextStackCursor$1更新为p。

getChildHostContextupdatedAncestorInfo校验dom嵌套结构的内容。

js 复制代码
function getChildHostContext(parentHostContext, type, rootContainerInstance) {
  {
    var parentHostContextDev = parentHostContext;
    var namespace = getChildNamespace(parentHostContextDev.namespace, type);
    //每个标签要重新计算祖先和当前节点的嵌套信息,作为上下文
    var ancestorInfo = updatedAncestorInfo(parentHostContextDev.ancestorInfo, type);
    return {
      namespace: namespace,
      ancestorInfo: ancestorInfo
    };
  }
}

push和参数

我们在push的时候提供的第二个参数value是第一个参数游标的最新的值。

js 复制代码
function push(cursor, value, fiber) {
  index++;
  valueStack[index] = cursor.current;//旧的

  {
    fiberStack[index] = fiber;
  }

  cursor.current = value;//最新的
}

pushTopLevelContextObject:
           cursor        value    fiber
push(contextStackCursor, context, fiber);//context是contextStackCursor的值
push(didPerformWorkStackCursor, didChange, fiber);//cursor是didPerformWorkStackCursor,value是didChange

pushHostContainer:
push(rootInstanceStackCursor, nextRootInstance, fiber); // Track the context and the Fiber that provided it.
push(contextFiberStackCursor, fiber, fiber); // Finally, we need to push the host context to the stack.
push(contextStackCursor$1, nextRootContext, fiber);

pushContextProvider:
push(contextStackCursor, memoizedMergedChildContext, workInProgress);
push(didPerformWorkStackCursor, didPerformWorkStackCursor.current, workInProgress);

pushHostContext:
push(contextFiberStackCursor, fiber, fiber);
push(contextStackCursor$1, nextContext, fiber);

cursor分别是contextStackCursor、didPerformWorkStackCursor...等游标
value是nextContext fiber didChange等,如祖先信息对象、Provider-Context的上下文对象
我们在push的时候提供的第二个参数value是第一个参数游标cursor的最新的值。
valueStack栈是全部游标的值,可以看到valueStack其实非常的乱,代码中也没有对valueStack栈的直接使用,都是使用contextStackCursor$1 contextStackCursor rootInstanceStackCursor 这些游标的值
fiberStack是全部fiber

实例

jsx 复制代码
import React from "react";
import PropTypes from "prop-types";

// 顶层 Provider,定义 childContextTypes
class ThemeProvider extends React.Component {
  // 定义能往下传的 context 类型
  static childContextTypes = {
    theme: PropTypes.string,
  };

  // 返回要传递下去的值(合并到父 context)
  getChildContext() {
    return { theme: "dark" };
  }

  render() {
    return this.props.children;
  }
}

// 中间组件,不消费 context,只是转发 children
class Layout extends React.Component {
  render() {
    return <div>{this.props.children}</div>;
  }
}

// 消费 context 的组件
class ThemedButton extends React.Component {
  // 声明需要订阅哪些 context
  static contextTypes = {
    theme: PropTypes.string, //过滤出需要使用的
  };

  render() {
    return <button>当前主题: {this.context.theme}</button>;
  }
}

export default function App() {
  return (
    <ThemeProvider>
      <Layout>
        <ThemedButton />
      </Layout>
    </ThemeProvider>
  );
}

源码 completeWork createInstance createTextInstance

js 复制代码
function completeWork(){
  case HostComponent:{
    popHostContext(workInProgress);
    //获取游标
    var rootContainerInstance = getRootHostContainer();
    var type = workInProgress.type;
    if (current !== null && workInProgress.stateNode != null) {
      updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);

      if (current.ref !== workInProgress.ref) {
        markRef$1(workInProgress);
      }
    }else{
      ...
      //获取游标
      var currentHostContext = getHostContext();
      var _wasHydrated = popHydrationState(workInProgress);
      if (_wasHydrated) {
        if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {
          markUpdate(workInProgress);
        }
      }else{
      //创建dom实例 里面会校验html标签嵌套
        var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
        appendAllChildren(instance, workInProgress, false, false);
        workInProgress.stateNode = instance; 
        if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
          markUpdate(workInProgress);
        }
      }
    }
  }
  case HostText:
  {
    var newText = newProps;

    if (current && workInProgress.stateNode != null) {
      var oldText = current.memoizedProps; 
      updateHostText$1(current, workInProgress, oldText, newText);
    } else {
      if (typeof newText !== 'string') {
        if (workInProgress.stateNode === null) {
          throw new Error('We must have new props for new mounts. This error is likely ' + 'caused by a bug in React. Please file an issue.');
        } // This can happen when we abort work.

      }

      var _rootContainerInstance = getRootHostContainer();
      var _currentHostContext = getHostContext();
      
      var _wasHydrated2 = popHydrationState(workInProgress);
      if (_wasHydrated2) {
        if (prepareToHydrateHostTextInstance(workInProgress)) {
          markUpdate(workInProgress);
        }
      } else {
        workInProgress.stateNode = createTextInstance(newText, _rootContainerInstance, _currentHostContext, workInProgress);
      }
    }

    bubbleProperties(workInProgress);
    return null;
  }
}
js 复制代码
// validateDOMNesting需要知道上下文
function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
  var parentNamespace;

  {
    //
    validateDOMNesting(type, null, hostContextDev.ancestorInfo);

    if (typeof props.children === 'string' || typeof props.children === 'number') {
      var string = '' + props.children;
      //validateDOMNesting需要知道上下文
      var ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type);
      validateDOMNesting(null, string, ownAncestorInfo);
    }

    parentNamespace = hostContextDev.namespace;
  }
//创建element
  var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
  precacheFiberNode(internalInstanceHandle, domElement);
  updateFiberProps(domElement, props);
  return domElement;
}
js 复制代码
// validateDOMNesting需要知道上下文
function createTextInstance(text, rootContainerInstance, hostContext, internalInstanceHandle) {
  {
    // validateDOMNesting需要知道上下文
    var hostContextDev = hostContext;
    validateDOMNesting(null, text, hostContextDev.ancestorInfo);
  }

  var textNode = createTextNode(text, rootContainerInstance);
  precacheFiberNode(internalInstanceHandle, textNode);
  return textNode;
}
js 复制代码
updateHostComponent$1 = function (current, workInProgress, type, newProps, rootContainerInstance) {
  var oldProps = current.memoizedProps;

  if (oldProps === newProps) {
    // In mutation mode, this is sufficient for a bailout because
    // we won't touch this node even if children changed.
    return;
  } 

  var instance = workInProgress.stateNode;
  var currentHostContext = getHostContext(); 

  var updatePayload = prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext); 

  workInProgress.updateQueue = updatePayload; 
  if (updatePayload) {
    markUpdate(workInProgress);
  }
};
js 复制代码
// validateDOMNesting需要知道上下文
function prepareUpdate(domElement, type, oldProps, newProps, rootContainerInstance, hostContext) {
  {
    var hostContextDev = hostContext;

    if (typeof newProps.children !== typeof oldProps.children && (typeof newProps.children === 'string' || typeof newProps.children === 'number')) {
      var string = '' + newProps.children;
      //validateDOMNesting需要知道上下文
      var ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type);
      validateDOMNesting(null, string, ownAncestorInfo);
    }
  }

  return diffProperties(domElement, type, oldProps, newProps);
}
js 复制代码
function requiredContext(c) {
  if (c === NO_CONTEXT) {
    throw new Error('Expected host context to exist. This error is likely caused by a bug ' + 'in React. Please file an issue.');
  }

  return c;
}
js 复制代码
function getRootHostContainer() {
  var rootInstance = requiredContext(rootInstanceStackCursor.current);
  return rootInstance;
}
js 复制代码
function getHostContext() {
  var context = requiredContext(contextStackCursor$1.current);
  return context;
}
相关推荐
方方洛几秒前
电子书阅读器:epub电子书文件的解析
前端·产品·电子书
idaibin1 分钟前
Rustzen Admin 前端简单权限系统设计与实现
前端·react.js
GISer_Jinger7 分钟前
Trae Solo模式生成一个旅行足迹App
前端·javascript
zhangbao90s8 分钟前
Intl API:浏览器原生国际化API入门指南
前端·javascript·html
艾小码10 分钟前
构建现代前端工程:Webpack/Vite/Rollup配置解析与最佳实践
前端·webpack·node.js
跟橙姐学代码15 分钟前
Python 集合:人生中最简单的真理,只有一次
前端·python·ipython
复苏季风17 分钟前
站在2025 年 来看,现在应该怎么入门CSS
前端·css
pepedd86418 分钟前
深度解剖 Vue3 架构:编译时 + 运行时的协作
前端·vue.js·trae
一枚前端小能手20 分钟前
🧪 改个代码就出Bug的恐惧,前端测试来帮忙
前端·测试
s3xysteak21 分钟前
我要成为vue高手02:数据传递
前端·javascript·vue.js