React Context 原理理解

Posted YuLong~W

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Context 原理理解相关的知识,希望对你有一定的参考价值。

文章目录

Context

React提供了 Context 上下文模式,为了解决 props 每层都需要传递的问题,即Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法

注意:提供者一定要是消费者的某一层父级

Context 相关用法

v16.3.0后,context api 正式发布

createContext 创建出一个 context 上下文对象,context 对象提供两个组件,ProviderConsumer作为新的提供者和消费者,这种 context 模式,更便捷的传递 context

1、React.createContext

const ThemeContext = React.createContext(null) //创建context对象
const ThemeProvider = ThemeContext.Provider  //提供者
const ThemeConsumer = ThemeContext.Consumer // 订阅消费者
  • createContext 接受一个参数,作为初始化 context 的内容,返回一个Context 对象
  • Context 对象上的 Provider 作为提供者,Context 对象上的 Consumer 作为消费者

2、提供者:Provider

const ThemeProvider = ThemeContext.Provider  //提供者
export default function ProviderDemo()
    const [ contextValue , setContextValue ] = React.useState(  color:'#ccc', background:'pink' )
    return <div>
        <ThemeProvider value= contextValue  > 
            <Son />
        </ThemeProvider>
    </div>

provider 作用:

  • value 属性传递 context,供给 Consumer 使用。
  • value 属性改变,ThemeProvider 会让消费 Provider value 的组件重新渲染。

Provider 特性总结:

  • Provider 作为提供者传递 context ,provider中value属性改变会使所有消费context的组件重新更新。

  • Provider可以逐层传递context,下一层Provider会覆盖上一层Provider。

3、消费者:Consumer

① 类组件之contextType 方式

const ThemeContext = React.createContext(null)
// 类组件 - contextType 方式
class ConsumerDemo extends React.Component
   render()
       const  color,background  = this.context
       return <div style= color,background   >消费者</div> 
   

ConsumerDemo.contextType = ThemeContext

const Son = ()=> <ConsumerDemo />
  • 类组件的静态属性上的 contextType 属性,指向需要获取的 context( demo 中的 ThemeContext ),就可以方便获取到最近一层 Provider 提供的 contextValue 值

② 函数组件之 useContext 方式

const ThemeContext = React.createContext(null)
// 函数组件 - useContext方式
function ConsumerDemo()
    const  contextValue = React.useContext(ThemeContext) 
    const  color,background  = contextValue
    return <div style= color,background   >消费者</div> 

const Son = ()=> <ConsumerDemo />
  • useContext 接受一个参数,就是 context 对象 ,返回一个 value 值,就是最近的 provider 提供 contextValue 值

③ 订阅者之 Consumer 方式

const ThemeConsumer = ThemeContext.Consumer // 订阅消费者

function ConsumerDemo(props)
    const  color,background  = props
    return <div style= color,background   >消费者</div> 

const Son = () => (
    <ThemeConsumer>
        /* 将 context 内容转化成 props  */ 
        (contextValue)=> <ConsumerDemo  ...contextValue  /> 
    </ThemeConsumer>
) 
  • Consumer 订阅者采取 render props 方式,接受最近一层 provider 中value 属性,作为 render props 函数的参数,可以将参数取出来,作为 props 混入 ConsumerDemo 组件,说白了就是 context 变成了 props。

总结:在 Provider 里 value 的改变,会使引用①contextType,②useContext 消费该 context 的组件重新 render ,同样会使 ③Consumer 的 children 函数重新执行,与前两种方式不同的是 Consumer 方式,当 context 内容改变的时候,不会让引用 Consumer 的父组件重新更新。

问题1:如何阻止 Provider value 改变造成的 children 不必要的渲染?

  • ① 第一种就是利用 memo,pureComponent 对子组件 props 进行浅比较处理。
const Son = React.memo(()=> <ConsumerDemo />)  
  • ② 第二种就是 React 本身对 React element 对象的缓存。React 每次执行 render 都会调用 createElement 形成新的 React element 对象,如果把 React element 缓存下来,下一次调和更新时候,就会跳过该 React element 对应 fiber 的更新。
<ThemeProvider value= contextValue  >
     React.useMemo(()=>  <Son /> ,[]) 
</ThemeProvider>

问题2:context 与 props 和 react-redux 的对比?

context 相比较 props 解决了:

  • 解决了 props 需要每一层都手动添加 props 的缺陷
  • 解决了改变 value ,组件全部重新渲染的缺陷

react-redux 就是通过 Provider 模式把 redux 中的 store 注入到组件中的

Context 原理

1、Context对象

通过 createContext 创建一个 context 。

  • Provider 本质上是一个 element 对象 $$typeof -> REACT_PROVIDER_TYPE
  • Consumer 本质上也是一个 element 对象 $$typeof -> REACT_CONTEXT_TYPE
  • _currentValue 这个用来保存传递给 Provider 的 value

2、Provider 提供者

Provider 本质上是一个特殊的 React Element 对象

jsx -> element -> fiber 关系:

  • <Provider> -> REACT_PROVIDER_TYPE React element -> ContextProvider fiber

在 fiber 调和阶段会进入到 beginWork 流程:

  • 如果当前类型的 fiber 不需要更新,那么会 FinishedWork 中止当前节点和子节点的更新。
  • 如果当前类型 fiber 需要更新,那么会调用不同类型 fiber 的处理方法。updateContextProvider 方法

1、updateContextProvider

  • 调用 pushProvider,会获取 type 属性上的 _context 对象,将 Provider 的 value 属性,赋值给 context 的 _currentValue 属性上。

  • 通过 calculateChangedBits 计算出 changedBits,当它返回的 changedBits === 0 的时候,那么还会浅比较 children 是否发生变化和有没有 legacy context,不需要更新,那么会 return 停止向下调和子节点

  • 否则,context改变,调用propagateContextChange进行更新

问:Proider 通过什么手段传递 context value?

答:即通过挂载 context 的 _currentValue 属性


2、propagateContextChange

  • 深度遍历所有的子代 fiber ,然后找到里面具有 dependencies 的属性。

  • 对比 dependencies 中的 context 和当前 Provider 的 context 是否是同一个,如果是,如果当前 fiber 是类组件,绑定一个 forceUpdate 标识 。

  • 然后会提高 fiber 的更新优先级,让 fiber 在接下来的调和过程中,处于一个高优先级待更新的状态。

  • 找到当前 fiber 向上的父级链上的 fiber ,统一更新他们的优先级,使之变成高优先级待更新状态。

  • 对高优先级的fiber进行 beginWork

dependencies 属性,这个属性可以把当前的 fiber 和 context 建立起关联,是一个链表结构,将 fiber 对应的 context 存放在 dependencies 中。

注意:一个 fiber 可以有多个 context 与之对应。

问题1、什么情况下 fiber 存在 dependencies—— 即 什么情况下使用 context

  • contextType 静态属性指向的类组件
  • 使用 useContext hooks 的函数组件
  • context 提供的 Consumer

问题2、 为什么class 组件会创建一个 ForceUpdate 的 update

context 要突破这些控制,就要做到当 value 改变,消费 context 的类组件更新,则需要通过 forceUpdate 强制更新。通过 PureComponentshouldComponentUpdate 等层层阻碍,解决了类组件更新限制。


3、beginWork 更新流程

React 会从 rootFiber 开始更新,每一个更新 fiber 都会走 beginWork 流程,开始找不同,找到有没有需要更新的地方,其中一个重要的寻找指标就是 更新的优先级

父子节点中形成了一条 root fiber -> 父 fiber -> 子 fiber 的 beginWork

  • 第一种: 如果遇到组件,而且 更新不涉及当前组件,也不在当前组件的父子递归链上,那么就不会 render,也不会向下 beginWork 。
  • 第二种: 如果遇到组件,而且 更新不涉及当前组件,但是更新组件属于当前组件的子孙后代,那么不会 render,但是会向下 beginWork ,目的很明确,找到对应的更新组件。
  • 第三种: 如果遇到 其他类型的 fiber 比如 hostComponent <div> ,那么会对比当前的更新优先级,如果低优先级,那么不需要向下 beginWork 。反之向下 beginWork。

总结流程如下:

  • 如果一个组件发生更新,那么当前组件到 fiber root 上的父级链上的所有 fiber ,更新优先级都会升高,都会触发 beginwork 。
  • render 不等于 beginWork,但是 render 发生,一定触发了 beginwork
  • 一次 beginwork ,一个 fiber 下的同级兄弟 fiber 会发生对比,找到任务优先级高的 fiber 。向下 beginwork 。

Provider 原理Provider 更新,会递归所有的子组件,只要消费了 context 的子代 fiber ,都会给一个高优先级。而且向上更新父级 fiber 链上的优先级,让所有父级 fiber 都处于一个高优先级。那么接下来高优先级的 fiber 都会 beginWork 。

多个 Provider 嵌套

如果有多个 Provider 的情况,那么后一个 contextValue 会覆盖前一个 contextValue,在开发者脑海中,要有一个定律就是:Provider 是用来传递 value,而非保存 value 。

3、Consumer 消费者

Consumer 本质上是类型为 REACT_CONTEXT_TYPE 的 element 对象。在调和阶段,会转化成 ContextConsumer 类型的 fiber 对象。

在 beginwork 中,会调用 updateContextConsumer 方法。

1、updateContextConsumer

  • 首先调用 readContext 获取最新的 value
  • 然后通过 render props 函数,传入最新的 value,得到最新的 children
  • 接下来 调和 children

fiber 上的 dependencies 如何和 context 建立起关联。

2、readContext

  • 首先会创建一个 contextItem
  • fiber 上会存在多个 dependencies ,它们以链表的形式联系到一起,如果不存在最后一个 context dependency , context dependencies 为空 ,那么会创建第一个 dependency 。
  • 如果存在 最后一个dependency,那么 contextItem 会以链表形式保存,并变成最后一个 lastContextDependency 。

useContext 和 contextType

1、useContext 原理,调用 useContext 本质上调用 readContext 方法。

  • 函数组件通过 readContext ,将函数组件的 dependencies和当前 context 建立起关联,context 改变,将当前函数组件设置高优先级,促使其渲染。

2、类组件的静态属性 contextType,本质上就是调用 readContext 方法。

  • 静态属性 contextType ,在类组件实例化的时候被使用,本质上也是调用 readContext将 context 和 fiber 上的 dependencies 建立起关联。

Context 原理

  • Provider 传递流程:Provider 的更新,会 深度遍历子代 fiber,消费 context 的 fiber 和父级链都会 提升更新优先级。 对于类组件的 fiber ,会 forceUpdate 处理。接下来所有消费的 fiber,都会 beginWork
  • Context 订阅流程contextTypeuseContextConsumer 会内部调用 readContext ,readContext 会把 fiber 上的 dependencies 属性context 对象 建立起关联。

总结

1、Context的用法

2、Context的原理

以上是关于React Context 原理理解的主要内容,如果未能解决你的问题,请参考以下文章

使用react context实现一个支持组件组合和嵌套的React Tab组件

react context原理

react context原理

React Context API似乎重新渲染每个组件

对象作为 React 子代无效(在 Internet Explorer 11 中用于 React 15.4.1)

React 库中的 context 和 updater 参数是啥?