react context原理
Posted coderlin_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react context原理相关的知识,希望对你有一定的参考价值。
带着问题思考:
- 1 Provder 如何传递 context?
- 2 三种获取 context 原理 ( Consumer, useContext,contextType )?
- 3 消费 context 的组件,context 改变,为什么会订阅更新 (如何实现) 。
- 4 context 更新,如何避免 pureComponent , shouldComponentUpdate 渲染控制策略的影响。
- 5 如何实现的 context 嵌套传递 ( 多个 Povider )?
context对象
首先context对象是通过React.createContext创建的,我们看看这个api
- 返回的就是一个对象,上面有Provider属性,本质其实就 Provider的elemetn类型。
- 而Consumer更是直接指向本身,本质就是一个Context的element类型。
- 上面还有其他的属性,比如_currentValue就是用来存放context的value的。
Provider
上面我们知道了provider就是一个特殊的react Element类型。那么我们重点看下Provider的实现原理。
围绕着两个点
- Provider如何传递context状态。
- Provider中value改变,如何订阅通知context。
首先看下demo
然后看看App执行beginWork,为provider这个儿子创建fiber时候的场景。
- 这个就是Provider组件的vdom,本质也是一个element,只不过type上面有特殊的标识,标识这是一个Provider组件,并且可以通过type._context获取到value值。
- 最终通过createFiberFromTypeAndProps为provider组件创建fiber的时候,
传入的type是一个对象。
最终走到这里逻辑,所以他的fiberTag就是ContextProvider
接着调用createFiber,返回fiber。此时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
大概三个步骤:
- 1 调用pushProvider,他会获取fiber上面的type,获取_context对象,将value赋值到context._currentValue属性上面。Provider就是通过这个来挂在value,即将value挂载到context._currentValue上面。
- 2 判断新老props的value是否改变,(浅比较),如果改变调用propagateContextChange函数,没改变则停止调和子节点。
- 3 如果改变,继续向下调和子节点。
重点看看propagateContextChange函数,它最终会调用propagateContextChange_eager函数
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,表示没有子节点,那么就得处理兄弟节点,比如
// <Provider><Son1/><Son2/></Provider> 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;
- 首先,从ProviderFiber.child开始,通过while循环遍历,从每个fiber.dependencies获取一个数组,里面存放着所有的context,因为context可能有多个。
- 遍历dependencies链表,如果当前context等于变化的context的时候,如果是类组件的话,当前创建一个update,并且将update的类型标记为forceUpdate(类似于调用this.forceUpdate带来的更新)。
- 更新自身fiber和父级以上的fiber的优先级。
- 继续遍历,如果没有儿子,那就遍历兄弟的,因为都属于Provider下面的节点。
这里可以罗列几个问题
fiber.dependencies是什么?
首先,fiber.dependencies存放着每个context,一个fiber可以有多个context与之对应,什么情况下会使用context呢?
1 有contextType静态属性指向的类组件
2 使用useContext的函数组件
3 使用了contet提供的Consumer
这里就可以推测,遇到上述3种的fiber,就会将context放入dependencies种。
为什么只针对类组件创建一个forceUpdate的update呢?
类组件如果要强制更新,就得通过PureComponent和shouldComponent等阻碍。而context要想突破这些限制,就比如做到当value改变,直接强制消费context的类组件更新,那么就需要通过forceUpdate了。
而这也解释了最开始的问题?context 更新,如何避免 pureComponent , shouldComponentUpdate 渲染控制策略的影响。
,就是通过value改变,对于Provider下面的儿孙子们,只要有一个消费了context的类组件,直接创建一个forceUpdate的update。
为什么要遍历父级更新fiber的优先级。
react更新机制的原因,如果此次更新可能发生在fiber树上某一叶子种,因为context穿透影响,react并不知道此次更新的波及范围。那么如何处理呢?其实跟setState触发更新react重新更新的机制是一样的。
- react会从RootFiber开始更新,每一个更新fiber都走一次beginWork,然后通过判断当前fiber.childLandes或者fiber.lanes是否等于此次更新的lane,以此来判断当前节点是否需要更新。
- 有三种情况,
1 如果遇到组件,但是fiber.childLanes和lanes都不等于,也就是说当前更新不涉及到该fiber所在的链上,那么就不会render,也不会向下beginWork。
2 如果遇到组件,fiber.lanes不等于,但是fiber.childLanes等于,也就是说当前更新属于该节点下面的某个节点。那么就不render,但是会向下beginWork,目的明显,就是为了找到对应的更新组件。
3 如果遇到hostComoonent如div,那么直接判断childLanes,如果不等于就退出,等于的话就继续向下beginWork。
以此我们就知道了,为什么当前fiber context变化的话,需要更新从该ifber到rootFiber上所有fiber的优先级,为的就是方便react更新的时候能顺利找到发生变化的fiber。
总结:
1 如果一个组件发生更新,那么当前组件到 fiber root 上的父级链上的所有 fiber ,更新优先级都会升高,都会触发 beginwork 。
2 render 不等于 beginWork,但是 render 发生,一定触发了 beginwork 。
3 一次 beginwork ,一个 fiber 下的同级兄弟 fiber 会发生对比,找到任务优先级高的 fiber 。向下 beginwork 。
Context更新的原理
只要Porvider上面的context发生变化,就会递归所有的子组件,只要是消费了context的fiber,都会给一个高优先级,并且向上更新父级fiber的优先级,然后react从rootFiber往下遍历,直到找到该fiber,进行更新。图所示:
发现context变化,类组件消费Context,提高优先级
react从rootFIber往下遍历,找到变化的fiber。
Consumer原理
上述说到了,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;
如上所示,其实做的事情就是
- 获取context,调用readContext获取最新的value。
- 然后通过render(value)将value作为参数传入,因为Consumer的children就是一个函数。得到children
- 继续调和children。
上面说到fiber是如何与context建立关联的,其实就是通过readContext。
如上,创建一个contextItem,多个contextItem通过链表关联。然后存放在fiber.dependencies上面,以此达到fiber和contex之间的联系。这样下次Provider更新的时候,才能遍历子孙组件,通过对比子孙组建的dependencies上面的context,找到需要更新的fiber,进行更新。
看完了Conusmer的原理,我们需要再了解一下contextType和useContext的原理。
其实也很简单
useContext
在保存hooks的对象中我们可以看到,useContext就是readContext,我们需要显示的传入context对象,才能获取value,并且readContext也会将该函数组件的fiber.dependencies和当前context建立关联,只要value改变,Provdier组件进行beginWork的时候,也能找到该函数组件进行标记,促使其渲染。
contextType原理
其原理和useContext一样,本质也是调用readContet
类组件创建实例的时候,如果遇到了有静态属性contextType的,就直接调用readContext,然后也会将该类组件建立与当前context的关系,方便更新。
Provider 嵌套传递原理。
知道了ifber怎么存放context对象之后,多个Provider嵌套的原理其实也明白了。多个provider嵌套的话,如果有订阅的,就会建立关联,多个context对象同时共存于fiber.dependience,然后该怎么更新就怎么更新。因为每个context跟fiber的关联逻辑就在那。并不影响。
Context流程总结
-
首先看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进阶实践指南》
以上是关于react context原理的主要内容,如果未能解决你的问题,请参考以下文章