【React源码系列】React Context原理,为什么我建议尽可能少的使用React Redux管理状态

发布于:2023-01-13 ⋅ 阅读:(249) ⋅ 点赞:(0)

> 这篇文章介绍了 react 中 context 的实现原理,以及 context 变化时,React 如何查找所有订阅了 context 的组件并跳过 shouldComponentUpdate 强制更新。可以让我们更加充分认识到 context 的性能瓶颈并能够合理设计全局状态管理。欢迎关注[mini-react](https://github.com/lizuncong/mini-react)一起学习react源码

## 学习目标

- React Context 的实现原理
- 订阅了 context 的组件是如何跳过`shouldComponentUpdate`强制 render 的
- React 是如何使用堆栈来存储 Provider 的 value 以支持嵌套 Provider 的
- context 的存取发生在 React 渲染的哪些阶段
- fiber.dependencies 用于保存当前组件订阅的 context 依赖,一般情况下组件只有一个 context 依赖。但是通过 useContext 订阅多个 context 时,fiber.dependencies 就是一个链表
- 为什么我建议尽量少的使用 React Redux?如何合理使用 React Redux 管理全局共享状态?

## 前言

先来简单回顾一下,React 的渲染分为两大阶段,五小阶段:

- render 阶段
  - beginWork
  - completeUnitOfWork
- commit 阶段。
  - commitBeforeMutationEffects
  - commitMutationEffects
  - commitLayoutEffects

beginWork 阶段主要是协调子元素,也就是常说的 dom diff。在 render 阶段,React 为每一个 fiber 节点调用 beginWork 开始执行工作,如果 fiber 没有子节点或者子节点都已经完成了工作,那么这个 fiber 就可以调用 completeUnitOfWork 完成自身的工作,这个过程就是深度优先遍历,具体可以看这篇文章[深入概述 React 初次渲染及状态更新主流程](https://github.com/lizuncong/mini-react/blob/master/docs/render/%E6%B7%B1%E5%85%A5%E6%A6%82%E8%BF%B0%20React%E5%88%9D%E6%AC%A1%E6%B8%B2%E6%9F%93%E5%8F%8A%E7%8A%B6%E6%80%81%E6%9B%B4%E6%96%B0%E4%B8%BB%E6%B5%81%E7%A8%8B.md)。

**React context 只作用于 beginWork 阶段。** 在 beginWork 阶段,如果当前组件订阅了 context,则从 context 中读取 value 值。

context 提供了一种存取全局共享数据的方式

## Context API 简介

React 提供的与 Context 相关的 API,按用途可以划分如下:

- 创建 context: React.createContext
- 提供 context 值: Context.Provider
- 订阅 context 值:
  - Class.contextType。用于类组件订阅 Context
  - Context.Consumer。用于函数组件订阅 Context,这种方式也可以间接的订阅多个 context
  - useContext。用于函数组件订阅 Context,唯一的订阅多个 context 的 api。通过 useContext 订阅多个 context 时,函数组件的 fiber.dependencies 就是一个链表

下面逐一介绍每个 API 的原理

## React.createContext

createContext 负责创建一个 context 对象,包含 Provider 和 Consumer 属性,其中 \_currentValue 用于存储全局共享状态,订阅了 context 的组件都是从 context.\_currentValue 中读取最新值的

```js
var symbolFor = Symbol.for;
const REACT_CONTEXT_TYPE = symbolFor("react.context");
const REACT_PROVIDER_TYPE = symbolFor("react.provider");
function createContext(defaultValue) {
  var context = {
    $$typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    Provider: null,
    Consumer: null,
  };
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = {
    $$typeof: REACT_CONTEXT_TYPE,
    _context: context,
  };

  return context;
}
const context = createContext({ count: 0 });
console.log("context....", context);
```

## Context.Provider

Provider 有三个特性:

- 如果没有对应的 Provider,那么消费组件将读取 context 的默认值,即传递给 createContext 的 defaultValue
- 多个 Provider 可以嵌套使用,里层的会覆盖外层的数据
- Provider 的 value 值发生变化时,它内部的所有消费组件都会跳过 shouldComponentUpdate 强制更新

在介绍 Provider 的源码实现前,我们思考一下,如果让我们设计一个类似 Provider 的 API,如何设计才能满足前面两个特性?(第三个特性机制较复杂,后面会详细介绍)

### 特性 1:Context 默认值的读取

如果没有对应的 Provider,那么消费组件将读取 context 的默认值,即传递给 createContext 的 defaultValue

注意,useContext(CounterContext)等价于 CounterContext.\_currentValue,为了减少干扰方便演示,这里我直接使用 CounterContext.\_currentValue 替代 useContext

```jsx
const CounterContext = React.createContext(-1);

const Counter = () => {
  // const context = useContext(CounterContext);
  const context = CounterContext._currentValue;
  return <div>{context}</div>;
};
class Home extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return <Counter />;
  }
}
```

由于没有 Provider,Counter 将读取 context 的默认值,即页面显示-1。但如果我们用 Provider 包裹一下:

```jsx
render() {
  return (
    <CounterContext.Provider value={1}>
      <Counter />
    </CounterContext.Provider>
  );
}
```

由于有 Provider,Counter 将读取 Provider 的 value 值,即页面显示 1。

在调用 React.createContext 创建 context 时,context.\_currentValue 的值保存的就是默认值。因此,如果没有 CounterContext.Provider 时,Counter 可以通过 context.\_currentValue 读取到默认值。

同理,如果有 CounterContext.Provider 包裹 Counter 组件时,我们只需要将 Provider 的 value 值保存到 context.\_currentValue 中就能让 Counter 读取到。

在 render 阶段,CounterContext.Provider 开始 beginWork 时,我们可以将 CounterContext.\_currentValue 设置为新的 value 值。这样在后续的渲染阶段,Counter 就能够通过 CounterContext.\_currentValue 读取到 Provider 最新的 value 值。我们似乎已经满足了第一个特性

```js
function beginWork(current, workInProgress, renderLanes) {
  switch (workInProgress.tag) {
    case ContextProvider:
      CounterContext._currentValue = workInProgress.pendingProps.value;
  }
}
```

但考虑到下面的案例

```jsx
render() {
  return [
    <CounterContext.Provider value={1}>
      <Counter />
    </CounterContext.Provider>,
    <Counter />,
  ];
}
```

第二个 Counter 由于没有 Provider,理论上它要读取 context 的默认值。但是我们在 beginWork 时,已经将 CounterContext.\_currentValue 修改成最新的值了,第二个 Counter 读取到的也将是最新的值,而不是默认值。我们需要修改一下 beginWork 的逻辑

```js
let valueCursor;
function beginWork(current, workInProgress, renderLanes) {
  switch (workInProgress.tag) {
    case ContextProvider:
      valueCursor = CounterContext._currentValue; // 先将旧值保存起来
      CounterContext._currentValue = workInProgress.pendingProps.value; // pendingProps保存的是新值
  }
}
```

我们声明一个全局变量,将旧值保存起来,然后再将 CounterContext.\_currentValue 设置成新的 value 值。那么问题来了,我们应该在哪个阶段将 CounterContext.\_currentValue 的值恢复成旧值?

> React 在 render 阶段会遍历每一个 fiber 节点并调用 beginWork 为 fiber 执行工作,如果 fiber 没有子节点或者子节点都已经完成了工作,那么可以调用 completeUnitOfWork 为 fiber 完成工作。这个过程就是深度优先遍历,我们可以将 beginWork 理解为"进入"fiber 节点,而将 completeUnitOfWork 理解为"离开"fiber 节点,因此我们可以在离开 fiber 节点时,将 context.\_currentValue 恢复成旧值

completeUnitOfWork 内部调用 completeWork 完成工作,大概如下:

```js
function completeWork(current, workInProgress, renderLanes) {
  switch (workInProgress.tag) {
    case ContextProvider:
      // 在离开Provider节点时,将context._currentValue恢复成旧值
      const oldValue = valueCursor;
      CounterContext._currentValue = oldValue;
      return null;
  }
}
```

### 特性 2:多个相同 Provider 可以嵌套使用,里层的会覆盖外层的数据

我们自己设计的 api 已经能够满足 Provider 的第一个特性了,我们在进入 Provider fiber 节点时,将当前的 context.\_currentValue 值保存起来,然后再将 Provider 新的 value 值保存在 context.\_currentValue 中,这样 Provider 内部的所有组件都能够通过 context.\_currentValue 读取到最新的值。然后再离开 Provider fiber 节点时,我们将 context.\_currentValue 恢复成旧值。

现在让我们考虑下面多个 Provider 嵌套的场景:

```jsx
render() {
  return [
    <CounterContext.Provider id="provider1" value={1}>
      <CounterContext.Provider id="provider2" value={2}>
        <Counter id="counter1" />
      </CounterContext.Provider>
      <Counter id="counter2" />
    </CounterContext.Provider>,
    <Counter id="counter3" />,
  ];
}
```

根据 Provider 里层覆盖外层值的特性:

- `counter1`读取的是`provider2`的 value 值,即 2
- `counter2`读取的是`provider1`的 value 值,即 1
- `counter3`由于没有 Provider 包裹,因此读取的是 context 的默认值,即-1

页面显示 2,1,-1。

**同时,我们必须要理解的一点是,`provider1`和`provider2`虽然是两个组件,但他们的 context 是同一个引用,三个 Counter 组件都是从 context.\_currentValue 中读取的值**

显然我们在前面设计的 api 满足不了这种场景,为了能实现嵌套的机制,我们遵循 React 遍历 fiber 节点的顺序,来看下这个思路:

- 在进入`provider1`时,将当前 context.\_currentValue 的值(记为`oldValue1`)保存起来,然后将`provider1`新的 value 值赋值给 context.\_currentValue
- React 继续遍历`provider2`,进入`provider2`时,我们又需要将当前 context.\_currentValue 的值(记为`oldValue2`)保存起来,然后将`provider2`新的 value 值赋值给 context.\_currentValue
- React 继续遍历`counter1`,`counter1`读取 context.\_currentValue 的值,就是`provider2`的 value 值。遍历完`counter1`后,就可以调用 completeUnitOfWork 完成工作
- 当为`provider2`完成工作时,即离开`provider2`时,我们需要将 context.\_currentValue 的值恢复成`provider1`的 value 值,即`oldValue2`
- 开始遍历 counter2,此时 counter2 通过 context.\_currentValue 读取到的就是 provider1 的 value 值
- 当为`provider1`完成工作时,即离开`provider1`时,我们需要将 context.\_currentValue 的值恢复成默认值,即`oldValue1`

看上去这个思路行得通,我们只需要多声明几个全局变量保存 context 的当前值就可以了

```js
let valueCursor1; // 保存oldValue1
let valueCursor2; // 保存oldValue2
function beginWork(current, workInProgress, renderLanes) {
  switch (workInProgress.tag) {
    case ContextProvider:
      if (workInProgress.id === "provider1") {
        valueCursor1 = CounterContext._currentValue; // 进入provider1时,将CounterContext的当前值保存起来,此时是默认值
        CounterContext._currentValue = workInProgress.pendingProps.value; //将provider1新的value值赋值给CounterContext._currentValue
      }
      if (workInProgress.id === "provider2") {
        valueCursor2 = CounterContext._currentValue; // 进入provider2时,将CounterContext的当前值保存起来,此时是provider1的value值
        CounterContext._currentValue = workInProgress.pendingProps.value; //将provider2新的value值赋值给CounterContext._currentValue
      }
  }
}
function completeWork(current, workInProgress, renderLanes) {
  switch (workInProgress.tag) {
    case ContextProvider:
      if (workInProgress.id === "provider2") {
        CounterContext._currentValue = valueCursor2; // 离开provider2节点时,需要将context._currentValue恢复成provider1的value值
      }
      if (workInProgress.id === "provider1") {
        CounterContext._currentValue = valueCursor1; // 离开provider1节点时,需要将context._currentValue恢复成默认值
      }

      return null;
  }
}
```

似乎这个实现思路已经满足两个 provider 的嵌套,如果有代码洁癖的同学会发现,valueCursor1,valueCursor2,以及 if 判断不太友好。同时,最重要的是,我们无法满足更多层级的 provider 的嵌套:

```jsx
  render() {
    return [
      <CounterContext.Provider id="provider1" value={1}>
        <CounterContext.Provider id="provider2" value={2}>
          <CounterContext.Provider id="provider3" value={3}>
            <CounterContext.Provider id="provider4" value={4}>
              <Counter id="counter1" />
            </CounterContext.Provider>
          </CounterContext.Provider>
        </CounterContext.Provider>
        <Counter id="counter2" />
      </CounterContext.Provider>,
      <Counter id="counter3" />,
    ];
  }
```

如果按照我们的做法,得声明好几个变量保存当前值,然后我们又无法确定有多少层级根本无法提前声明这些变量。看到这里,fiber 节点进进出出的很容易让我们想到堆栈,我们可以使用栈来保存当前的 context 值

```js
let valueStack = [];
let index = -1;
function beginWork(current, workInProgress, renderLanes) {
  switch (workInProgress.tag) {
    case ContextProvider: {
      var context = workInProgress.type._context;
      index++;
      valueStack[index] = context._currentValue; // 先将context当前的值保存起来
      context._currentValue = workInProgress.pendingProps.value; // 然后将provider新的value值赋值给context._currentValue
    }
  }
}
function completeWork(current, workInProgress, renderLanes) {
  switch (workInProgress.tag) {
    case ContextProvider: {
      const preValue = valueStack[index];
      valueStack[index] = null;
      index--;
      var context = providerFiber.type._context;
      context._currentValue = preValue;
    }
  }
}
```

这个版本的实现已经可以满足嵌套任意层的 Provider 了,同时还能满足不同的 Provider 组件嵌套,比如:

```jsx
render() {
  return [
    <CounterContext.Provider id="provider1" value={1}>
      <UserContext.Provider id="userprovider1" value={"mike"}>
        <Counter id="counter1" />
      </UserContext.Provider>
    </CounterContext.Provider>,
  ];
}
```

实际上,这正是 React 所采用的实现方式,这种方式既能满足读取默认值的特性,又能满足里层的 Provider 覆盖外层的 Provider 的场景(指的是相同的 Provider 的覆盖)

### Context.Provider 源码实现

React 在 beginWork 阶段对 Provider 类型的 fiber 节点执行的主要工作有两点:

- 调用 pushProvider,将 context.\_currentValue 保存到 valueStack 栈中。然后将 context.\_currentValue 设置成 Provider 新的 value 值
- 使用浅比较判断 Context.Provider 的新旧 value 值是否发生了改变,如果发生了改变,则调用 propagateContextChange 找出所有订阅了这个 context 的组件,然后跳过 shouldComponenentUpdate 强制更新。查找算法放在后面单独一节介绍

在 completeWork 阶段,Provider 类型的 fiber 节点执行的主要工作有一点:

- 调用 popProvider 将 context.\_current 恢复成上一个值,即直接从 valueStack 取出第一项即可

```js
function beginWork(current, workInProgress, renderLanes) {
  switch (workInProgress.tag) {
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
  }
}
function completeWork(current, workInProgress, renderLanes) {
  var newProps = workInProgress.pendingProps;
  switch (workInProgress.tag) {
    case ContextProvider:
      popProvider(workInProgress);
      return null;
  }
}
var valueCursor = { current: null };
var valueStack = [];
var index = -1;
function pushProvider(providerFiber, nextValue) {
  var context = providerFiber.type._context;
  index++;
  valueStack[index] = valueCursor.current;
  valueCursor.current = context._currentValue;
  context._currentValue = nextValue;
}
function updateContextProvider(current, workInProgress, renderLanes) {
  var context = workInProgress.type._context; // React.createContext的返回值
  var newProps = workInProgress.pendingProps;
  var oldProps = workInProgress.memoizedProps;
  var newValue = newProps.value;

  pushProvider(workInProgress, newValue);

  if (oldProps !== null) {
    var oldValue = oldProps.value;
    var changedBits = newValue === oldValue;
    if (changedBits === 0) {
      if (oldProps.children === newProps.children && !hasContextChanged()) {
        // 没有改变
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes
        );
      }
    } else {
      // context变了,遍历Provider所有的子孙fiber节点,查找订阅了该context的组件并标记为强制更新
      propagateContextChange(workInProgress, context, changedBits, renderLanes);
    }
  }

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

function popProvider(providerFiber) {
  var currentValue = valueCursor.current;
  pop(valueCursor);
  var context = providerFiber.type._context;
  context._currentValue = currentValue;
}
```

## 消费组件如何读取 Context 的值?

React 提供了三种方式读取 Context 的值:

- Class.contextType。用于类组件订阅 Context
- Context.Consumer。用于函数组件订阅 Context
- useContext。用于函数组件订阅 Context

以上三种方式,都是需要手动传递 context 对象的,比如 useContext(context),Class.contextType = context。

对于函数组件,useContext 本质上就是调的`readContext`函数,即我们可以直接认为`useContext === readContext`,readContext 返回 context.\_currentValue

对于类组件,React 会判断类组件上是否有静态属性 contextType,如果有,则调用 `readContext` 读取 context 值,并赋值给类实例的 context 属性

```js
const ctor = workInProgress.type;
const instance = new ctor();
const contextType = ctor.contextType;
if (typeof contextType === "object" && contextType !== null) {
  instance.context = readContext(contextType);
}
```

对于 Context.Consumer,context 本身就存在 Consumer 里面

```js
var context = workInProgress.type;
var newProps = workInProgress.pendingProps;
var render = newProps.children;
prepareToReadContext(workInProgress, renderLanes);
var newValue = readContext(context, newProps.unstable_observedBits);
var newChildren = render(newValue);
```

可以看出,这三种方式在读取 context 时都要进行两个操作:

- 在读取 context 前,都需要先调用`prepareToReadContext`进行准备工作,重置几个和 contex 有关的全局变量,以及判断 context 的 value 是否变更了
- 都是调用 readContext 方法读取 context 值,readContext 方法返回 context.\_currentValue 的值

`prepareToReadContext`主要逻辑如下:

- 将全局变量 currentlyRenderingFiber 设置为当前正在工作的 fiber,在 readContext 时可以通过这个全局变量拿到正在工作中的 fiber
- 将全局变量 lastContextWithAllBitsObserved 重置为 null,这个变量在 readContext 函数中会被设置成 context 对象
- 全局变量 lastContextDependency 在通过 useContext 订阅多个不同的 context 时,用于构造 dependencies 列表
- 重置 fiber dependencies 列表

```js
function prepareToReadContext(workInProgress, renderLanes) {
  currentlyRenderingFiber = workInProgress;
  lastContextDependency = null;
  lastContextWithAllBitsObserved = null; // 这个全局变量保存的是context对象
  var dependencies = workInProgress.dependencies;

  if (dependencies !== null) {
    var firstContext = dependencies.firstContext;

    if (firstContext !== null) {
      if (includesSomeLane(dependencies.lanes, renderLanes)) {
        // Context list has a pending update. Mark that this fiber performed work.
        didReceiveUpdate = true;
      } // Reset the work-in-progress list
      // 重置fiber context依赖
      dependencies.firstContext = null;
    }
  }
}
```

### readContext 读取 context 最新值

context 本质上就是一个全局变量,我们完全可以在函数组件或者类组件中通过`context._currentValue`直接访问 context 值,比如:

```js
const Counter = () => {
  return <div>{CounterContext._currentValue}</div>;
};
```

不信你可以在代码中试试。虽然我们可以直接读取值,但这又引入了两个问题:

- context 的值变了,如何通知所有读取 context 的组件强制刷新?
- 怎么知道哪些组件订阅了 context?

为了解决这两个问题,React 引入 Provider,Provider 判断 value 变化,就会通知所有订阅了 context 的组件。同时通过 readContext 读取值,在读取的时候,通过在 fiber.dependencies 中添加 context,标记这个组件订阅了 context。

readContext 的逻辑也比较简单,首先判断 `lastContextWithAllBitsObserved === context`,如果相等,说明是同一个 context,这种判断是为了防止重复,readContext 的一个主要目标就是收集组件依赖的所有 context,比如:

```jsx
const CounterContext = React.createContext(-1);
const UserContext = React.createContext("mike");

const Counter = () => {
  const context = useContext(CounterContext);
  const context2 = useContext(CounterContext);
  const usercontext = useContext(UserContext);

  return (
    <div>
      {context}
      {usercontext}
    </div>
  );
};
```

这个例子中,React 认为 Counter 组件订阅了两个 context,而不是三个,因此将这两个 context 添加到 fiber 的 dependencies 依赖链表中,最终,fiber.dependencies 长这样:

```js
fiber.dependencies = {
  lanes,
  firstContext: {
    context: CounterContext,
    next: {
      context: UserContext,
      next: null,
    },
  },
  responders,
};
```

readContext 收集依赖的算法如下:

```js
function readContext(context, observedBits) {
  if (lastContextWithAllBitsObserved === context) {
  } else {
    lastContextWithAllBitsObserved = context;
    var resolvedObservedBits = MAX_SIGNED_31_BIT_INT;

    var contextItem = {
      context: context,
      observedBits: resolvedObservedBits,
      next: null,
    };

    if (lastContextDependency === null) {
      // 这是第一个依赖
      lastContextDependency = contextItem;
      currentlyRenderingFiber.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
        responders: null,
      };
    } else {
      // 添加到dependencies表尾
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }

  return context._currentValue;
}
```

## Context.Provider value 变化,React 如何强制更新?

在 Provider 的 value 值变化时,React 会遍历 Provider 内部所有的 fiber 节点,然后查看其 fiber.dependencies,如果 dependencies 中存在一个 context 和当前 Provider 的 context 相等,那说明这个组件订阅了当前的 Provider 的 context,需要将其标记为强制更新

先来看下面的 demo

```jsx
const CounterContext = React.createContext({
  count: 0,
  addCount: () => {},
});

class Counter extends React.Component {
  static contextType = CounterContext;
  shouldComponentUpdate() {
    return false;
  }
  render() {
    console.log("Counter render");
    return (
      <button id="counter" onClick={this.context.addCount}>
        {this.context.count}
      </button>
    );
  }
}

class CounterWrap extends React.Component {
  render() {
    console.log("CounterWrap render,控制台只会输出一次");
    return <Counter />;
  }
}

class NeverUpdate extends React.Component {
  render() {
    console.log("NeverUpdate render,控制台只会输出一次");
    return <div>永远不会更新</div>;
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);
  }
  shouldComponentUpdate() {
    return false;
  }
  render() {
    console.log("App render,控制台只会输出一次");
    return [<CounterWrap />, <NeverUpdate />];
  }
}

class Home extends React.Component {
  constructor(props) {
    super(props);
    this.addCount = () => {
      console.log("点击按钮触发更新", this.state.count + 1);
      this.setState({
        count: this.state.count + 1,
      });
    };
    this.state = {
      count: 0,
      addCount: this.addCount,
    };
  }

  render() {
    console.log("Home render");
    return (
      <CounterContext.Provider value={this.state}>
        <App />
      </CounterContext.Provider>
    );
  }
}
ReactDOM.render(<Home />, document.getElementById("root"));
```

- App 组件的 shouldComponentUpdate 永远返回 false,理论上 App 组件以及它的所有子组件的 render 在更新的过程中都不会执行,即不会更新。
- Counter 组件的 shouldComponentUpdate 永远返回 false,理论上 Counter 也不会更新

但是我们点击按钮,观察控制台可以发现:


![context-01.jpg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/32a0689568f540f6921279e743925d1b~tplv-k3u1fbpfcp-watermark.image?)

- 第一次渲染时,所有组件都会更新,组件的 render 方法都被执行
- 在随后的点击过程中,只有 Home 组件以及订阅了 Context 的 Counter 组件更新了,它们对应的 render 方法执行

Home 的更新很容易理解,因为点击按钮触发了它的 state 更新,**那么 Counter 组件是如何跳过父组件 App 以及其自身的 shouldComponentUpdate 强制更新的?**

前面介绍过在`updateContextProvider`方法中,使用浅比较判断 Provider 的 value 是否变化,如果变化,则调用`propagateContextChange`查找所有订阅了这个 context 的组件

### propagateContextChange 查找算法

以下面的代码为例:

```jsx
class Home extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1,
    };
  }

  render() {
    return [
      <CounterContext.Provider id="provider1" value={this.state.count + 1}>
        <div id="wrap">
          <CounterContext.Provider id="provider2" value={2}>
            <Counter id="counter1" />
          </CounterContext.Provider>
          <UserContext.Provider id="userprovider1" value="mike">
            <Counter id="counter2" />
          </UserContext.Provider>
        </div>
        <Counter id="counter3" />
      </CounterContext.Provider>,
      <button
        onClick={() => {
          this.setState({
            count: this.state.count + 1,
          });
        }}
      >
        click
      </button>,
    ];
  }
}
```

当点击按钮触发更新时,`provider1`的 value 发生变更,因此调用`propagateContextChange`开始查找所有订阅了`provider1`的 context 的 fiber 节点,按以下顺序:

- 首先是 `div#wrap`,由于它没有订阅了 CounterContext,因此没有任何操作,继续遍历它的子节点
- 由于 `provider2` 和 `provider1` 是同一个 context,因此不需要继续遍历`provider2`内部的子节点,因为即使`provider2`内部有组件订阅了 CounterContext,那也是读取的是 `provider2` 的 value 值,而不是 `provider1` 的 value 值,因此 `provider1` 的值发生变化不会影响到 `provider2` 内部的消费组件
- 继续遍历 `userprovider1`,没有订阅 CounterContext,因此继续遍历`couter2`,发现`counter2`订阅了 CounterContext,因此将其标记为更新
  - 如果 counter2 是类组件,那么会创建一个更新对象 update,并将 update.tag 标记为强制更新
- 继续遍历 counter3,发现 counter3 也订阅了 CounterContext

如果找到订阅了 context 的消费组件,则将其 fiber.lane 标记为更新,然后合并到父节点。

比如在我们上面那个例子中,Counter 需要更新,但是 App、CounterWrap、NeverUpdate 都不需要更新,因此这三个 fiber 节点在 beginWork 阶段会直接跳过,然后更新 Counter 组件。

至于怎么标记更新,这涉及到 fiber lane 的知识,就不在本节的讨论范围

**可以发现,当有 Provider 的 value 发生变化时,React 会遍历这个 Provider 内部所有的 fiber 节点,找出订阅了这个 Provider 的 context 的 fiber 节点。这个查找的过程也是挺耗时的,特别是组件层级很深时**

最后,propagateContextChange 查找算法如下:

```js
function propagateContextChange(
  workInProgress,
  context,
  changedBits,
  renderLanes
) {
  var fiber = workInProgress.child;

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

    var list = fiber.dependencies;
    // 首先判断这个fiber是否有dependencies,如果没有,说明这个fiber没有订阅任何context
    if (list !== null) {
      nextFiber = fiber.child;
      var dependency = list.firstContext;
      // 如果这个fiber有订阅context,则判断是否是当前Provider的context
      while (dependency !== null) {
        // 检查context是否匹配
        if (
          dependency.context === context &&
          (dependency.observedBits & changedBits) !== 0
        ) {
          // 匹配到了context,说明这个组件订阅了当前Provider的context,我们需要给这个fiber调度更新
          if (fiber.tag === ClassComponent) {
            // 如果是类组件,则创建一个更新对象update,并标记为强制更新
            var update = createUpdate(
              NoTimestamp,
              pickArbitraryLane(renderLanes)
            );
            update.tag = ForceUpdate;
            // 添加到更新队列
            enqueueUpdate(fiber, update);
          }

          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          var alternate = fiber.alternate;

          if (alternate !== null) {
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }

          scheduleWorkOnParentPath(fiber.return, renderLanes);

          list.lanes = mergeLanes(list.lanes, renderLanes);

          break;
        }

        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
      // 如果是相同的Provider,则不用继续遍历了,因为相同的嵌套的Provider,内部的消费组件取最里层的,外层的Provider变化
      // 和里面的消费组件没啥关系
      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
    } else {
      // 继续遍历子节点
      nextFiber = fiber.child;
    }

    if (nextFiber === null) {
      // 没有子节点,则继续遍历兄弟节点
      nextFiber = fiber;

      while (nextFiber !== null) {
        if (nextFiber === workInProgress) {
          // 所有fiber节点已经遍历完成,退出
          nextFiber = null;
          break;
        }

        var sibling = nextFiber.sibling;

        if (sibling !== null) {
          nextFiber = sibling;
          break;
        }
        // 如果没有兄弟节点,则查找父节点的兄弟节点
        nextFiber = nextFiber.return;
      }
    }

    fiber = nextFiber;
  }
}
```

## 如何合理使用 React Redux 管理全局共享状态

从上面 Provider 的 value 变化,查找所有订阅组件的过程可以看出,每次 Provider 一变化,都要遍历一次,像下面的代码:

```js
<CounterContext.Provider value={this.state.count}>
  <UserContext.Provider value={this.state.count + "mike"}>
    <App />
  </UserContext.Provider>
</CounterContext.Provider>
```

如果 this.state.count 发生变化,则导致在 beginWork 阶段:

- CounterContext.Provider 的 value 发生了变化,则遍历内部所有的 fiber 节点找出消费组件
- UserContext.Provider 的 value 也发生了变化,则遍历内部所有的 fiber 节点找出消费组件

如果页面很复杂,组件层级很深数量庞大,这个开销也是很大的。

因此,我们应该尽量少的避免 Provider 的 value 发生变化

在使用 React Redux 时,每次 dispatch 触发状态变更,React 都要查找一次。我们应该要尽可能少的使用 React Redux 管理状态,只在必要的时候,比如全局共享数据,才使用 React Redux 托管状态。而页面级别或者组件级别的状态应该在组件内部闭环,通过 this.state 或者 useState 管理
 

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到