带着问题思考:
首先context对象是通过React.createContext创建的,我们看看这个api
上面我们知道了provider就是一个特殊的react Element类型。那么我们重点看下Provider的实现原理。
围绕着两个点
首先看下demo
然后看看App执行beginWork,为provider这个儿子创建fiber时候的场景。
{
alternate: null
elementType: {
"$$typeof": Symbol("react.provider")
_context: {…}
}
firstEffect: null
lanes: 1
memoizedProps: null
mode: 8
nextEffect: null
pendingProps: {
children: {...}
value: {test: 1}
}
ref: null
return: null
sibling: null
stateNode: null
tag: ContextProvider
type: {
"$$typeof": Symbol("react.provider")
_context: {…}
}
updateQueue: null
}
现在我们知道了Provider fiber长啥样了,现在看看当Provider fiber进入beginWork的时候,会走什么逻辑?
对于ContextProvider,会执行updateContextProvider
大概三个步骤:
重点看看propagateContextChange函数,它最终会调用propagateContextChange_eager函数
简化后的函数
function propagateContextChange_eager(workInprogress, contextn, renderLanes){
let fiber = workInProgress.child;
while (fiber !== null) {
let nextFiber;
// Visit this fiber. 每个context存放在fiber.dependencies上面
const list = fiber.dependencies;
if (list !== null) {
nextFiber = fiber.child;
let dependency = list.firstContext;
while (dependency !== null) {
// 遍历所有context,因为context可能有多个
// 如果该context是当前变化的context
if (dependency.context === context) {
// 如果是类组件
if (fiber.tag === ClassComponent) {
const lane = pickArbitraryLane(renderLanes);
const update = createUpdate(NoTimestamp, lane);
update.tag = ForceUpdate;
// 创建Update,并且将他标记为forceUpdate
// 插入fiber.updateQueue钟
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
} else {
const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
const pending = sharedQueue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
}
// 将当前子节点的fiber的优先级更新
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
// 向上遍历父级fiber的childLanes
scheduleContextWorkOnParentPath(
fiber.return,
renderLanes,
workInProgress,
);
// Mark the updated lanes on the list, too.
list.lanes = mergeLanes(list.lanes, renderLanes);
// Since we already found a match, we can stop traversing the
// dependency list.
break;
}
dependency = dependency.next;
}
...}
if (nextFiber !== null) {
nextFiber.return = fiber;
// 如果nextFiber为null,表示没有子节点,那么就得处理兄弟节点,比如
// son1处理之后就得处理son2
} else {
// No child. Traverse to next sibling.
nextFiber = fiber;
while (nextFiber !== null) {
if (nextFiber === workInProgress) {
// We're back to the root of this subtree. Exit.
nextFiber = null;
break;
}
const sibling = nextFiber.sibling;
if (sibling !== null) {
// Set the return pointer of the sibling to the work-in-progress fiber.
sibling.return = nextFiber.return;
nextFiber = sibling;
break;
}
// No more siblings. Traverse up.
nextFiber = nextFiber.return;
}
}
// 遍历条件
fiber = nextFiber;
}
这里可以罗列几个问题
首先,fiber.dependencies存放着每个context,一个fiber可以有多个context与之对应,什么情况下会使用context呢?
1 有contextType静态属性指向的类组件
2 使用useContext的函数组件
3 使用了contet提供的Consumer
这里就可以推测,遇到上述3种的fiber,就会将context放入dependencies种。
类组件如果要强制更新,就得通过PureComponent和shouldComponent等阻碍。而context要想突破这些限制,就比如做到当value改变,直接强制消费context的类组件更新,那么就需要通过forceUpdate了。
而这也解释了最开始的问题?context 更新,如何避免 pureComponent , shouldComponentUpdate 渲染控制策略的影响。
,就是通过value改变,对于Provider下面的儿孙子们,只要有一个消费了context的类组件,直接创建一个forceUpdate的update。
react更新机制的原因,如果此次更新可能发生在fiber树上某一叶子种,因为context穿透影响,react并不知道此次更新的波及范围。那么如何处理呢?其实跟setState触发更新react重新更新的机制是一样的。
以此我们就知道了,为什么当前fiber context变化的话,需要更新从该ifber到rootFiber上所有fiber的优先级,为的就是方便react更新的时候能顺利找到发生变化的fiber。
总结:
1 如果一个组件发生更新,那么当前组件到 fiber root 上的父级链上的所有 fiber ,更新优先级都会升高,都会触发 beginwork 。
2 render 不等于 beginWork,但是 render 发生,一定触发了 beginwork 。
3 一次 beginwork ,一个 fiber 下的同级兄弟 fiber 会发生对比,找到任务优先级高的 fiber 。向下 beginwork 。
只要Porvider上面的context发生变化,就会递归所有的子组件,只要是消费了context的fiber,都会给一个高优先级,并且向上更新父级fiber的优先级,然后react从rootFiber往下遍历,直到找到该fiber,进行更新。图所示:
发现context变化,类组件消费Context,提高优先级
react从rootFIber往下遍历,找到变化的fiber。
上述说到了,Consumer其实就是context对象本身,而context对象本身就是一个element镀锡,类型为React_CONTEXT_TYPE。那么看看该对象作为组建的话,在beginWork的操作。
对于COnsumer组件,
他的fiberTag就是ContextConsumer,对应的在beginWork阶段调用的函数就是
function updateContextConsumer(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
let context: ReactContext<any> = workInProgress.type; //获取context对象
context = (context: any)._context; //获取context对象
const newProps = workInProgress.pendingProps; //即将更新的props
const render = newProps.children; //得到render , consumer的children就是一个函数,参数就是value
/* 读取 context */
prepareToReadContext(workInProgress, renderLanes);
// 通过context对象获取到最新的value
const newValue = readContext(context);
let newChildren;
// 将新的context通过props传给render,得到最新的vdom
newChildren = render(newValue);
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork; //打上标记
// 开始根据新的子vdom调和子fiber
reconcileChildren(current, workInProgress, newChildren, renderLanes);
// 返回儿子
return workInProgress.child;
}
如上所示,其实做的事情就是
上面说到fiber是如何与context建立关联的,其实就是通过readContext。
如上,创建一个contextItem,多个contextItem通过链表关联。然后存放在fiber.dependencies上面,以此达到fiber和contex之间的联系。这样下次Provider更新的时候,才能遍历子孙组件,通过对比子孙组建的dependencies上面的context,找到需要更新的fiber,进行更新。
看完了Conusmer的原理,我们需要再了解一下contextType和useContext的原理。
其实也很简单
在保存hooks的对象中我们可以看到,useContext就是readContext,我们需要显示的传入context对象,才能获取value,并且readContext也会将该函数组件的fiber.dependencies和当前context建立关联,只要value改变,Provdier组件进行beginWork的时候,也能找到该函数组件进行标记,促使其渲染。
其原理和useContext一样,本质也是调用readContet
类组件创建实例的时候,如果遇到了有静态属性contextType的,就直接调用readContext,然后也会将该类组件建立与当前context的关系,方便更新。
知道了ifber怎么存放context对象之后,多个Provider嵌套的原理其实也明白了。多个provider嵌套的话,如果有订阅的,就会建立关联,多个context对象同时共存于fiber.dependience,然后该怎么更新就怎么更新。因为每个context跟fiber的关联逻辑就在那。并不影响。
首先看createContext函数,返回了一个context对象,value值存放在了_currentValue上,而还提供了Provider对象和Consumer对象,本质也是react elemetn对象。
其次对于Provider组件,每次进行beginWork的时候,都会判断当前的value是否改变,如果改变了,那么他会遍历所有的子孙fiber,如果遇到了消费context的fiber(通过获取fiber.dependencies上的contextItem对象),如果是类组件,那么直接创建一个forceUpdate的update,为的就是避免PureComoent和shouldComponentUpdate的影响。如果是其他组件,比如函数组件,或者是Consumer组件,就会提升其优先级,并且将fiber到rootFiber所有的fiber的chilLanes也提升优先级。
对于Context的订阅一共有三种,useContext, Consumer, contextType,本质上都是调用readContext来建立fiber与context的关联(context对象通过链表存放在fiber.dependience上面),然后返回最新的value。
只有订阅了context的fiber,才会建立关联,那么value改变的时候,Provider组件进行beginWork的时候才能找到订阅了context的fiber,而对其他没有订阅的fiber不会影响。
文章通过学习掘金的《react进阶实践指南》作为笔记产出,文中图片部分来自《react进阶实践指南》