著有《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
};
}
游标和栈示例
游标分类
- 旧版的Context API用到的游标
- contextStackCursor 把Context作为上下文对象
- didPerformWorkStackCursor 把Context是否变化了作为上下文对象
- 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`。
- 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(综合)
- 旧版的Context API用到的游标
- pushContextProvider 设置contextStackCursor和didPerformWorkStackCursor
- invalidateContextProvider 设置contextStackCursor和didPerformWorkStackCursor
- pushTopLevelContextObject 设置HostRoot的contextStackCursor和didPerformWorkStackCursor
- 场景:tag=HostRoot类型的节点,pushTopLevelContextObject。
旧版的类组件、使用了Context的场景使用,pushContextProvider和invalidateContextProvider联合使用。
pushContextProvider是为了尽早push,放入的是上一次旧值,invalidateContextProvider会先把旧的拿出来,放入这一轮新的。
这一轮先放入了上一轮旧的,尽早放入值,确保堆栈的深度、长度、层级是对的,但是值可能是过期的。
- DOM嵌套信息和这个DOM对应Fiber
- pushHostContext设置contextStackCursor$1和contextFiberStackCursor。
- 场景:HostComponent类型的Fiber节点使用
- HostRoot和HostPortal类型的Fiber节点,它们的容器DOM节点
- ❌ 没有独立的设置rootInstanceStackCursor游标的函数
- 综合设置游标-设置3个游标
- pushHostContainer 设置rootInstanceStackCursor 和 contextFiberStackCursor和contextStackCursor$1。
- 场景:HostPortal 、pushHostRootContext(),需要综合设置3个游标,HostRoot需要设置5个游标。
- 设置全部游标
- pushHostRootContext
- 场景:HostRoot类型的Fiber节点需要一次性设置全部游标
源码 6个入栈函数
- 旧版的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);
}
}
- 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);
}
- 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);
}
- 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);
}
为什么要栈、为什么要游标
因为处在现在的值时需要知道上一个值。
- 旧版Context的嵌套,按照正确的顺序获取Context
- DOM嵌套,校验html嵌套是否合法
- 容器嵌套,...
对应游标
- 最新的Context的值
- 当前层级DOM节点的嵌套信息
- 当前的容器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--;
}
用例
因为只有这几种游标,所以对于简单的场景,我们可以直接知道结果:
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种游标。