react中context原理解析

495 阅读7分钟

一、context是什么

引用react官网中的话:Context 是一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。很好理解。context就是react中共享状态的api。且具有跨越层级的能力。

何时使用context,引用官网的话是:Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

作者认为简单的共享状态需求完全可以使用context实现。如果遇到较复杂的需求可以考虑使用redux,mobx等社区方案。

二、如何使用context

react通过使用createContext生成一个context对象。其会暴露出提供者Provider。消费者Consumer。

  • Provider接收一个 value 属性,传递给消费组件。

三种消费context的方式

  • Consumer可以让你在函数组件中订阅context
  • useContext也可以让你在函数组件中使用context
  • contextType可以让你在类组件中通过使用this.context来获取最近的context

列子

// context文件
import { createContext } from "react";
const rootContext = createContext(null)
export default rootContext

// 容器组件
function App () {
  return <Provider value={{
    name: 'huyunkun'
  }}>
    <Lib />
  </Provider>
}

// 子组件
import React, { useContext } from 'react'

export default function Lib () {
    const contextData = useContext(rootContext) || {}
    const { name } = contextData

    return <div>huyunkun</div>
}

对于context的使用作者在此不过多的阐述。有兴趣的通过可以阅读官网文档。该文默认读者熟练使用context。该文的侧重点为context的实现原理

三、context原理解析

我们从列子中可以看到context对象是通过createContext生成的。也就是说createContext是“梦”开始的地方。那么让我们来看看“梦“长什么样子。

function createContext (defaultValue) {
 
  var context = {
    $$typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    _currentValue2: defaultValue, 
    _threadCount: 0,
    // These are circular
    Provider: null,
    Consumer: null,
    // Add these to use same hidden class in VM as ServerContext
    _defaultValue: null,
    _globalName: null
  };
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context
  };

  {
   
    var Consumer = {
      $$typeof: REACT_CONTEXT_TYPE,
      _context: context
    }; 

    context.Consumer = Consumer;
  }

  {
    context._currentRenderer = null;
    context._currentRenderer2 = null;
  }

  return context;
}

我们可以看到createContext方法创建了一个context对象并放回出来。接下来看看context中的重要属性

  • _currentValue:用来存储Provider传入的value(这里有两个_currentValue,主要是考虑到并发渲染)
  • Provider:提供者
  • Consumer:消费者

我们看到Provider和Consumer是两个React Element对象,且其中的_context都是指向了context

Provider属性

对于Provider的研究我们的主要关注点在于

  • Provider如何传递context状态
  • Provider的中value改变了,如果通知消费者更新组件

阅读上文可知,Provider本质上是一个react Element对象。所以我们使用jsx语法写的Provider会变为一个React Element对象。React Element对象最终变为一个fiber对象。该fiber的tag会被赋值为ContextProvider。在react的reconcile阶段会进入到beginWork中。并在beginWork中处理ContextProvider类型的fiber节点。

整体数据结构流转为 jsx -> React Element -> Fiber

那接下来让我们看看beginWork中是如何处理ContextProvider类型的fiber。

function beginWork (current, workInProgress, renderLanes) {
    // ...省略
    switch (workInProgress.tag) {
        // ...省略
        case ContextProvider:
         return updateContextProvider(current, workInProgress, renderLanes);
        // ...省略
    }
    // ...省略
}
function updateContextProvider (current, workInProgress, renderLanes) {
  var providerType = workInProgress.type;
  var context = providerType._context;
  var newProps = workInProgress.pendingProps;
  var oldProps = workInProgress.memoizedProps;
  var newValue = newProps.value; // Provider上新的value

  // 将新的value 赋值给context
  pushProvider(workInProgress, context, newValue);

  {
    if (oldProps !== null) {
      var oldValue = oldProps.value;

      if (objectIs(oldValue, newValue)) {
        // value值未改变直接进入bailout逻辑
        // No change. Bailout early if children are the same.
        if (oldProps.children === newProps.children && !hasContextChanged()) {
          return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
        }
      } else {
        // The context value changed. Search for matching consumers and schedule
        // them to update.
        // value如果改变了则开始查找需要更新的子孙路径
        propagateContextChange(workInProgress, context, renderLanes);
      }
    }
  }

  var newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

我们看到ContextProvider类型的处理会进入到updateContextProvider函数中。

  1. 该函数中通过pushProvider将Provider中新的value赋值给context.
  2. 然后比较oldProps与newProps是否相等,和是否有context变化,判断是否进入bailout逻辑。
  3. 如果value有变化则会进入到propagateContextChange函数中。通过propagateContextChange执行propagateContextChange_eager函数。
function propagateContextChange_eager (workInProgress, context, renderLanes) {

  while (fiber !== null) {
    var nextFiber = void 0; // Visit this fiber.

    // fiber 中存放context依赖项的列表
    var list = fiber.dependencies;

    if (list !== null) {
      nextFiber = fiber.child;
      var dependency = list.firstContext;
    
      // 向下遍历找到消费了该context的所有子孙节点
      while (dependency !== null) {
        // Check if the context matches.
        if (dependency.context === context) {
          // Match! Schedule an update on this fiber.
          if (fiber.tag === ClassComponent) {
            // Schedule a force update on the work-in-progress.
            var lane = pickArbitraryLane(renderLanes);
            var update = createUpdate(NoTimestamp, lane);
            // 类组件添加上ForceUpdate 进行强制更新
            update.tag = ForceUpdate; // TODO: Because we don't have a work-in-progress, 

          }
           // ...省略
          // 向上遍历修改路径上的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;
      }
    } 


    fiber = nextFiber;
  }
}

由于该函数比较长,此处只贴出了核心部分。让我们一起来看一看:

该函数主要分为两部

  1. 从当前fiber开始向下递归遍历,找出所有的子fiber,并比较子fiber中dependencies的context和当前Provider的context是否相同,如果相同且fiber类型为类组件类型,则会给类组件类型标记上ForceUpdate的标记。该标记表示强制刷新类组件。然后会提高fiber的优先级。让fiber能继续调和。
  2. 接下来会从该fiber开始向上找出父级fiber,并提高该链路上所有父级fiber的优先级。

如此一来,从根节点一直往下所有的需要调和的fiber节点都提高了优先级。在接下来的调和中便会按照此链路调和。看下图列子:

stateDiagram-v2

App --> com1
App --> com2
com1 --> App
com1 --> com3
com3 --> com1
com1 --> com4

假设有如上一棵fiber树,并且在com1上消费了app上的提供的Provider,则会按照上图顺序遍历fiber树。且app。com1,com3的优先级都会提高等待更新。

介绍完了Provider的作用和流程。我们会发现一个问题。

  1. 上文中说到的dependencies是哪里来的?且dependencies是如何与context建立联系的。

我们在上文中说过。消费context可以使用Consumer,useContext,,contextType。因此我们可以联想到必然在这三个api中建立了与context的联系。我们用Consumer举列:

 case ContextConsumer:
          return updateContextConsumer(current, workInProgress, renderLanes);

我们可以看到,类似Provider,在beginWork中Consumer是作为ContextConsumer类型处理。我们看到updateContextConsumer函数。

function updateContextConsumer (current, workInProgress, renderLanes) {
  var context = workInProgress.type; // The logic below for Context differs depending on PROD or DEV mode. In

  var newProps = workInProgress.pendingProps;
  // 获取子fiber(该处为一个函数)
  var render = newProps.children;

  prepareToReadContext(workInProgress, renderLanes);
  // 获取最新的context,并且与context建立关系
  var newValue = readContext(context);

  {
    markComponentRenderStarted(workInProgress);
  }

  var newChildren;

  {
    newChildren = render(newValue);
  }

  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

代码删除了一部分,提取了关键逻辑。

我们可以看到updateContextConsumer的核心逻辑分为两部

  1. 通过readContext获取到最新的value。(且在readContext与context建立关系)
  2. 通过调用render函数,并传入最新的value,得到children。

我们先看到readContext函数:

function readContext (context) {

  // 读取_currentValue 最新的value值
  var value = context._currentValue;

  // 如果没有dependencies 则构建一个contextItem加到dependencies链表中
  if (lastFullyObservedContext === context); else {
    var contextItem = {
      context: context,
      memoizedValue: value,
      next: null
    };

    if (lastContextDependency === null) {
      if (currentlyRenderingFiber === null) {
        throw new Error('Context can only be read while React is rendering. ' + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + 'In function components, you can read it directly in the function body, but not ' + 'inside Hooks like useReducer() or useMemo().');
      } // This is the first dependency for this component. Create a new list.


      lastContextDependency = contextItem;
      currentlyRenderingFiber.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem
      };
    } else {
      // Append a new context item.
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }

  return value;
}

该函数干了两件事:

  1. 先读取_currentValue,也就是最新的value值。并将value值返回。
  2. 判断fiber上是否存在dependencies,如果不存在,则构建一个contextItem,加入到dependencies链表中。如果存在则将构建的contextItem加入到链表的最后。

这样消费则便和提供者建立起了联系。

其实useContext,contextType也是通过readContext来建立联系的,原理相同。

useContext: function (context) {
  currentHookNameInDev = 'useContext';
  mountHookTypesDev();
  return readContext(context);
}
if (typeof contextType === 'object' && contextType !== null) {
    context = readContext(contextType);
}

多个 Provider 嵌套

如果存在多个Provider的情况,则会取离当前fiber最近的一个Provider的值。

四、总结流程

最后让我们一起来总结一下context的执行流程:

  • Consumer,useContext,contextType通过readContext与Provider建立联系。并在fiber上创建了dependenciesdependencies以链表的形式保存不同的Provider。
  • Provider中的value改变时,会向下遍历所有的子fiber,找出与当前Provider相同的context。消费该context的父级fiber都会更新优先级。如果遇到类组件则给类组件打上ForceUpdate标记。

今天就到这里。欢迎大家评论区讨论。点赞。感谢各位读者