本文假设你对 context 基础用法和 React fiber 渲染流程有一定的了解,因为这些知识不会介绍详细。本文基于 React v18.2.0
React 渲染分为 render 阶段和 commit 阶段,其中 render 阶段分为两步(深度优先遍历)
:
beginWork
(进入节点的过程向下遍历,协调子元素)completeUnitOfWork
(离开节点的过程向上回溯)区别 render 和 beginWork
为了避免与上面的阶段混淆,以下 render
都代指开发者层面的 render
,即指类组件执行 render
方法或函数组件执行
beginWork
,但执行 beginWork
,不代表触发了组件的 render
(fiber 会检查组件是否需要进行渲染,不需要则会跳过复用旧的 fiber 节点)所以 render
不等于 beginWork
render
执行了,则一定经历了 beginWork
流程,触发了 beginWork
综上 beginWork
的工作是进入节点时协调子元素,如果 fiber 类型是类组件或者函数组件,则需检测比较组件是否需要执行 render
,不需要则会跳过复用旧的 fiber 节点
const MyContext = React.createContext(defaultValue)
创建一个 Context 对象。只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效
源码位置:packages/react/src/ReactContext.js
createContext 函数的核心逻辑是返回一个 context 对象,其中包括三个重要属性:
Provider
和 Consumer
两个组件(React Element 对象)属性_currentValue
:保存 context 的值,用来保存传递给 Provider 的 value 属性)下列是精简去除类型定义和引入的源码,后面源码举例都这么处理,为了方便直观的看:
const REACT_PROVIDER_TYPE = Symbol.for('react.provider')
const REACT_CONTEXT_TYPE = Symbol.for('react.context')
export function createContext(defaultValue) {
const context = {
$$typeof: REACT_CONTEXT_TYPE, // 本质就是 Consumer Element 类型
_currentValue: defaultValue, // 保存 context 的值
_currentValue2: defaultValue, // 为了支持多个并发渲染器,适配不同的平台
_threadCount: 0, // 跟踪当前有多少个并发渲染器
Provider: null,
Consumer: null,
}
// 添加 Provider 属性,本质就是 Provider Element 类型
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
}
// 添加 Consumer 属性
context.Consumer = context
return context
}
JSX 语法在进入 render 时会被编译成 React Element 对象
<MyContext.Provider value={/* 某个值 */}>
先来了解 Provider 的特性:
shouldComponentUpdate
强制更新如果一个组件发生更新,那么当前组件到 fiber root 上的父级链上的所有 fiber,更新优先级都会升高,都会触发 beginWork
,但不一定会 render
当初次 Fiber 树渲染,进入 beginWork
方法,其中对应的节点处理函数是 updateContextProvider
:
function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes)
}
}
进入 updateContextProvider 方法:
function updateContextProvider(current, workInProgress, renderLanes) {
const providerType = workInProgress.type
const context = providerType._context
const newProps = workInProgress.pendingProps
const oldProps = workInProgress.memoizedProps
// 新的 value 值
const newValue = newProps.value
// 获取 Provider 上的 value
pushProvider(workInProgress, context, newValue)
// 更新阶段
if (oldProps !== null) {
const oldValue = oldProps.value
// 使用 Object.is 来比较新旧值是否发生变化
if (is(oldValue, newValue)) {
// context 值没有变更,则提前退出
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
)
}
} else {
// context 值发生改变,深度优先遍历查找 consumer 消费组件,标记更新
propagateContextChange(workInProgress, context, renderLanes)
}
}
// 继续向下调和子代 fiber
const newChildren = newProps.children
reconcileChildren(current, workInProgress, newChildren, renderLanes)
return workInProgress.child
}
// 使用栈存储 context._currentValue 值,设置 context._currentValue 为最新值
function pushProvider(providerFiber, context, nextValue) {
// 压栈
push(valueCursor, context._currentValue, providerFiber)
// 修改 context 的值
context._currentValue = nextValue
}
workInProgress.pendingProps.value
值作为最新值,然后调用 pushProvider
方法设置context._currentValue
值pushProvider
:存储 context 值的函数,利用栈先进后出的特性,先把 context._currentValue
压栈;与后面流程的 popProvider
(出栈)函数相对应。Object.is
)来判断新旧 context 值是否发生改变,没发生改变则调用 bailoutOnAlreadyFinishedWork
进入 bailout,复用当前 Fiber 节点,改变则调用propagateContextChange
方法我们总结下 Context.Provider
的 Fiber 更新方法 —— updateContextProvider的核心逻辑
:
context._currentValue
(压栈)Object.is
浅比较 context 新旧值是否发生变化propagateContextChange
走更新的流程,深度优先遍历查找消费组件来标记更新propagateContextChange 逻辑:深度优先遍历所有的子代 fiber ,然后找到里面具有 dependencies 的属性,对比 dependencies 中的 context 和当前 Provider 的 context 是否是同一个,如果是同一个,会提高 fiber 的更新优先级,让 fiber 在接下来的调和过程中,处于一个高优先级待更新的状态,而高优先级的 fiber 都会 beginWork
由上文知识我们简略粗暴的说:Provider 一顿操作核心就是修改 context._currentValue
的值,那么消费 Context 值的原理也就是想方设法读取 context._currentValue
的值了。
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
一个 React 组件可以订阅 context 的变更,此组件可以让你在函数式组件中可以订阅 context。这种方法需要一个函数作为子元素(function as a child)。这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 value 值等价于组件树上方离这个 context 最近的 Provider 提供的 value 值
当 context 值更新时,Fiber 树渲染时,进入 beginWork
方法,beginWork
中对于 ContextConsumer
的节点处理函数是 updateContextConsumer:
function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes)
}
}
updateContextConsumer
的核心逻辑:
prepareToReadContext
和 readContext
读取最新的 context 值。function updateContextConsumer(current, workInProgress, renderLanes) {
// 拿到 context
let context = workInProgress.type
context = context._context
const newProps = workInProgress.pendingProps
// 获取 Consumer 组件的 render props children
const render = newProps.children
// 读取 context 前的准备工作
prepareToReadContext(workInProgress, renderLanes)
// 读取最新 context._currentValue 值
const newValue = readContext(context)
let newChildren
// 最新的 children element
newChildren = render(newValue)
// 进入主流程,调和 children
reconcileChildren(current, workInProgress, newChildren, renderLanes)
return workInProgress.child
}
const value = useContext(MyContext)
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。
看如下代码,useContext Hook 挂载阶段和更新阶段,本质都是调用 readContext
函数,readContext
函数会返回 context._currentValue
。而且也是调用了 prepareToReadContext
和 readContext
function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case FunctionComponent:
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
)
}
}
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps,
renderLanes,
) {
prepareToReadContext(workInProgress, renderLanes)
// 处理各种hooks逻辑
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
)
// ...
}
renderWithHooks 函数是调用函数组件的主要函数
function renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
) {
// ...
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount // 挂载阶段
: HooksDispatcherOnUpdate // 更新阶段
}
// 确保 Hooks 只能在函数组件内部或自定义 Hooks 中使用,提供正确的调度程序
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current
return dispatcher
}
function useContext(Context) {
const dispatcher = resolveDispatcher()
return dispatcher.useContext(Context)
}
const HooksDispatcherOnMount = {
useContext: readContext,
// ...
}
const HooksDispatcherOnUpdate = {
useContext: readContext,
// ...
}
class MyClass extends React.Component {
componentDidMount() {
let value = this.context
/* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
}
componentDidUpdate() {
let value = this.context
/* ... */
}
componentWillUnmount() {
let value = this.context
/* ... */
}
render() {
let value = this.context
/* 基于 MyContext 组件的值进行渲染 */
}
}
MyClass.contextType = MyContext
挂载在 class 上的 contextType 属性可以赋值为由 React.createContext() 创建的 Context 对象。此属性可以让你使用 this.context 来获取最近 Context 上的值。你可以在任何生命周期中访问到它,包括 render 函数中。
contextType
readContext
方法,并赋值给类实例的 context 属性,所以我们才可以使用 this.context 获取 context 值function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case ClassComponent:
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
)
}
}
function updateClassComponent(
current,
workInProgress,
Component,
nextProps,
renderLanes,
) {
// ...
prepareToReadContext(workInProgress, renderLanes)
mountClassInstance(workInProgress, Component, nextProps, renderLanes)
// ...
}
function mountClassInstance(workInProgress, ctor, newProps, renderLanes) {
// ...
const instance = workInProgress.stateNode
// 判断类组件上是否有静态属性 contextType
const contextType = ctor.contextType
// 有则调用 readContext
if (typeof contextType === 'object' && contextType !== null) {
// 赋值给类实例的 context 属性
instance.context = readContext(contextType)
}
}
综上,以上三种方式只是 React 根据不同使用场景封装的 API,它们在消费/订阅 context 的共同操作:
prepareToReadContext
进行准备工作readContext
方法读取 context 值(readContext
方法返回 context._currentValue
最新值)上文提到 propagateContextChange
,如果组件订阅了 context,不管是函数组件还是类组件,都会将 fiber.lanes
设置为 renderLanes
。在 beginWork
阶段,发现 fiber.lanes
等于 renderLanes
,则走 beginWork
的逻辑,强制组件更新
prepareToReadContext 的核心逻辑:
currentlyRenderingFiber
为当前工作的 fiber,并重置lastContextDependency
等全局变量function prepareToReadContext(workInProgress, renderLanes) {
// 设置全局变量 currentlyRenderingFiber 为当前工作的 fiber, 为 readContext 做准备
currentlyRenderingFiber = workInProgress
// 用于构造 dependencies 列表
lastContextDependency = null
// 将全局变量 lastFullyObservedContext (保存的是 context 对象) 重置为 null
lastFullyObservedContext = null
const dependencies = workInProgress.dependencies
if (dependencies !== null) {
const firstContext = dependencies.firstContext
if (firstContext !== null) {
if (includesSomeLane(dependencies.lanes, renderLanes)) {
// Context list has a pending update. Mark that this fiber performed work.
markWorkInProgressReceivedUpdate()
}
// 重置 fiber context 依赖
dependencies.firstContext = null
}
}
}
readContext 的核心逻辑:
fiber.dependencies
链表中context._currentValue
, 并构造一个contextItem
添加到workInProgress.dependencies
链表之后。function readContext(context) {
return readContextForConsumer(currentlyRenderingFiber, context)
}
function readContextForConsumer(consumer, context) {
// ReactDOM 中 isPrimaryRenderer 为 true,则一直返回 context._currentValue
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2
// 相等说明是同一个 Context,不处理为了防止重复添加依赖
if (lastFullyObservedContext === context) {
// Nothing to do. We already observe everything in this context.
} else {
const contextItem = {
context: context,
memoizedValue: value,
next: null,
}
// 构造一个 contextItem, 加入到 workInProgress.dependencies 链表之后
if (lastContextDependency === null) {
lastContextDependency = contextItem
// dependencies 属性用于判定是否依赖了 ContextProvider 中的值
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
}
} else {
// 将 context 添加到 fiber.dependencies 链表末尾
lastContextDependency = lastContextDependency.next = contextItem
}
}
// 返回 context._currentValue
return value
}
上面源码实际上还是讲解不够完整的,在这推荐一篇文章:【React 源码系列】React Context 原理,如何合理设计共享状态,个人认为相对讲得很清晰了。
想知道自己对原理的理解,除了输出就是回答解决一些提问了,这里列举了一些原理相关的问题,写下简略的解答,看看自己是否了解。
通过将 Provider 的 value 属性值赋值给 context._currentValue
render() {
return (
<>
<TestContext.Provider value={10}>
{/* 可读到 context 值最新值 10 */}
<Test />
</TestContext.Provider>
{/* 只能读到 context 初始值(createContext 函数的参数 defaultValue) */}
<Test />,
</>
)
}
消费 context 时是读取 context._currentValue
值,理论上其它组件也是读取该最新值的。Provider 其中一个特性是只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue
参数才会生效。所以没有被 Provider 包裹的组件,是只能读到默认值的。
React 在深度优先遍历 fiber 树时,最外层 Provider 开始 beginWork
,会先将 context._currentValue
的旧值保存起来,赋新的值给 context._currentValue
(所以在里层的组件都能读到最新值),在离开 Provider 节点时会调用 completeUnitOfWork
完成工作,在此会将 context._currentValue
恢复成旧值,到遍历第二个 <Test />
节点时就读的是 context 的默认值(不被 Provider 包裹的组件 render 时 beginWork
的时候就读到旧值了)。
render() {
return (
<>
<TestContext.Provider value={10}>
<Test1 />
<TestContext.Provider value={100}>
<Test2 />
</TestContext.Provider>
</TestContext.Provider>
</>
)
}
在这场景下, <Test1 />
和 <Test2 />
组件读取的值分别是 10 和 100。
为了实现嵌套的机制,React 利用的是栈的特性(后入先出),通过 pushProvider
和 popProvider
。
Fiber 深度优先遍历时:
pushProvider
,此时栈顶是 10pushProvider
,此时栈顶是 100,即context._currentValue
的值为 100消费组件 <Test2 />
读取时,在其所在 Provider 范围内先读取栈顶的值,所以读取的是 100;里层的 Provider 完成遍历工作离开时,弹出栈顶 popProvider
的值 100,此时栈顶的值是 10, 即 context._currentValue
的值为 10,<Test1 />
里面读到的值也就为 10 了。
由于 React 调和过程就是 Fiber 树深度优先遍历的过程, 向下遍历(beginWork)和向上回溯(completeWork)恰好符合栈的特性(入栈和出栈),Context 的嵌套读取就是利用了这个特性。
readContext
方法REACT_CONTEXT_TYPE
的 React Element 对象,context 本身就存在 Consumer 里面,本质也是调用 readContext
readContext
三种方式只是 React 根据不同使用场景封装的 API,本质都是调用了 readContext
方法读取 context._currentValue
值
context 的存取就是发生在 beginWork
阶段,在 beginWork
阶段,如果当前组件订阅了 context,则从 context 中读取 _currentValue
值
updateContextProvider
方法,里面的 propagateContextChange
方法会对 fiber 子树向下深度优先遍历所有的 fiber 节点,目的是为了找到消费组件标记更新。如果 fiber.dependencies
中存在一个 context 和当前 Provider 的 context 相等,那说明这个组件订阅了当前的 Provider 的 context,就会被标记更新。readContext
方法则会把 fiber.dependencies
和 context 对象建立关联,fiber.dependencies
用于判断是否依赖了 ContextProvider
中的值fiber.childLanes
属性,(childLanes
属性用于判断子节点是否需要更新)需要更新则子节点就会进入更新逻辑(开始 beginWork
)。PureComponent
和 shouldComponentUpdate
等优化策略,在外部代码层面,我们可调用 this.forceUpdate()
,就会给类组件打上强制更新的 tag。而在内部实现上, context 的 value 改变时,要想订阅 context 的类组件更新,相应的也得打上强制更新的 tagpropagateContextChange
对 Fiber 子树向下深度优先遍历所有的 fiber 节点,如果 fiber.dependencies
中存在一个 context 和当前 Provider 的 context 相等,那说明这个组件订阅了当前的 Provider 的 context,如果 fiber 节点是类组件, 则会创建一个 update 对象,并将 update.tag
标记为 ForceUpdate
;而处理 update 时,发现 tag 为 ForceUpdate
的话,会将全局变量 hasForceUpdate
设置为 true, 这决定了类组件会强制更新。在 updateClassComponent 中会调用 updateClassInstance 判断类组件是否应该更新。在 updateClassInstance 中会判断全局变量 hasForceUpdate 或者组件的 shouldComponentUpdate 的返回值是否为 true, true 则表示要强制更新。
Context 的实现原理:
createContext
返回一个 context 对象,对象包括 Provider
和 Consumer
两个组件属性,并创建 _currentValue
属性用来保存 context 的值readContext
读取 context._currentValue
获取最新值ContextProvider
节点深度优先遍历子代 fiber,消费 context 的 fiber 和父级链都会提升更新优先级;对于类组件的 fiber ,会被 forceUpdate
处理。接下来所有消费的 fiber,都会执行 beginWork
阅读量:189
点赞量:0
收藏量:0