React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React Context

一文带你深入理解React中的Context

作者:Gamble_

React Context是React提供给开发者的一种常用的状态管理机制,本文主要来和大家讲讲为什么需要Context,又是如何使用Context的,感兴趣的可以了解一下

前言

React Context是React提供给开发者的一种常用的状态管理机制,利用Context可以有效的将同一个状态在多级组件中进行传递,并能够在状态更新时,自动的通知各个组件进行更新。那React Context又是如何做到这一点的,以及为什么需要这么设计呢?

为什么需要Context

在React的数据管理理念中,一直遵循着单项数据流以及数据不变性的理念。当我们需要从父组件将状态向子组件传递时,我们往往需要通过Props显式进行传递,例如:

const Father:FC = () => {
  const [count, setCount] = useState<number>(0)
  return (
    <Son count={count} />
  )
}
const Son:FC = (props) => {
  const { count } = props;
  return (
    <span>{count}</span>
  )
}

但是,倘若父组件需要向子组件的子组件,也就是孙组件进行状态的传递呢?或者父组件需要同时向多个子组件进行传递呢?当然,继续使用props进行逐层往下的显示传递肯定也是能实现这个需求的,但那样的代码未免过于繁琐且难以维护,如果能够在父组件里维护一个类似于Js里的全局变量,所有的子组件都能使用这个全局变量不就好了吗?

是的,这个就是Context的作用,但又远远不止这么简单。

Context是什么

Context提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

Context如何使用

创建Context

首先,我们需要在父组件中, 利用React.createContext创建一个React Context对象,这个方法接受一个入参,作为当前Context的默认值。

import React from 'react'
const Context = React.createContext(defaultValue)

向下传递数据

利用Context对象返回的Provide组件,包裹需要传递数据的子组件。

每个 Context 对象都会返回一个 Provider React 组件,它接收一个 value 属性,可将数据向下传递给消费组件。当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。

const Father:FC = () => {
  const [count, setCount] = useState<number>(0)
  return (
    <Context.Provider value={count}>
    	<Son />
    </Context.Provider>
  )
}

接收数据

被包裹的子组件,利用useContext获取父组件传递的数据。

const Son:FC = (props) => {
  const value = React.useContext(Context);
  return (
    <span>{value}</span>
  )
}

Context如何以及为何这样实现

让我们回到Context使用过程的第一步,通过阅读源码去研究createContext究竟做了什么样的工作?

剔除了一些干扰代码,其实createContext做的事情其实非常简单,创建了一个对象,保存了当前context的value, 以及返回了一个Provide组件。

import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import type {ReactProviderType} from 'shared/ReactTypes';
import type {ReactContext} from 'shared/ReactTypes';
export function createContext<T>(defaultValue: T): ReactContext<T> {
  // TODO: Second argument used to be an optional `calculateChangedBits`
  // function. Warn to reserve for future use?
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    // As a workaround to support multiple concurrent renderers, we categorize
    // some renderers as primary and others as secondary. We only expect
    // there to be two concurrent renderers at most: React Native (primary) and
    // Fabric (secondary); React DOM (primary) and React ART (secondary).
    // Secondary renderers store their context values on separate fields.
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    // Used to track how many concurrent renderers this context currently
    // supports within in a single renderer. Such as parallel server rendering.
    _threadCount: 0,
    // These are circular
    Provider: (null: any),
    Consumer: (null: any),
    // Add these to use same hidden class in VM as ServerContext
    _defaultValue: (null: any),
    _globalName: (null: any),
  };
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  return context;
}

在React编译的过程中,会将我们写的JSX语法代码,转化成React.createElement方法,执行这个方法后,会得到一个ReactElement元素对象,也就是我们所说的Virtual Dom。这个元素对象,会记录着当前组件所接收的入参以及元素类型。

而Provide组件实际上编译完之后也是一个ReactElement,只不过他的Type跟正常的组件并不一样,而是context.Provider。

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };

那么,子组件又是如何利用Provider和useContext去获取到最新的数据的呢?

useContext接收一个context对象作为参数,从context._currentValue中读取当前contetx的value值。

function readContextForConsumer<T>(
  consumer: Fiber | null,
  context: ReactContext<T>,
): T {
  // 获取当前context保存的value
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;
	// ...do something
  // 返回当前的值
  return value;
}

问题又来了,当父组件的状态改变时,又是如何通过Provider触发更新,通知订阅当前状态的子组件进行重新渲染的呢?

当父组件的状态进行更新时,React整体会进入到调度更新阶段,Fiber节点会进入到beginWork的方法当中,在这个方法里面,会根据当前更新节点的类型,从而执行相对应的方法。上文提到,Provider组件是有单独的自己的类型ContextProvider的,所以会进入到相对应的更新方法,updateContextProvide。

其实updateContextProvide里做的事情,大抵可以概括为:

首先更新context._currentValue, 然后比较新老value是否发生改变,如果没有发生改变,则跳出更新函数,复用当前fiber节点。如果发生了改变,则调用一个叫propagateContextChange的方法,对该Provider组件的子组件进行深度遍历,找到订阅了当前context的子组件,并打上需要更新的标记,lane。

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  const providerType: ReactProviderType<any> = workInProgress.type;
  const context: ReactContext<any> = providerType._context;
  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;
  const newValue = newProps.value;
  pushProvider(workInProgress, context, newValue);
  if (enableLazyContextPropagation) {
    // In the lazy propagation implementation, we don't scan for matching
    // consumers until something bails out, because until something bails out
    // we're going to visit those nodes, anyway. The trade-off is that it shifts
    // responsibility to the consumer to track whether something has changed.
  } else {
    if (oldProps !== null) {
      const oldValue = oldProps.value;
      if (is(oldValue, newValue)) {
        // No change. Bailout early if children are the same.
        if (
          oldProps.children === newProps.children &&
          !hasLegacyContextChanged()
        ) {
          return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderLanes,
          );
        }
      } else {
        // The context value changed. Search for matching consumers and schedule
        // them to update.
        propagateContextChange(workInProgress, context, renderLanes);
      }
    }
  }
	// do something...
}

那么, 在深度遍历的时候,又是如何知道当前子组件是否有订阅当前Context的呢?

其实在使用useContext的时候,除了读取当前context的value,还会把接收的context对象信息保存在当前组件的Fiber.dependencies上,所以在遍历的时候,只需要看当前组件的dependencies上有没有当前context便可以知道当前组件是否存在订阅关系了。

function readContextForConsumer<T>(
  consumer: Fiber | null,
  context: ReactContext<T>,
): T {
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;
  if (lastFullyObservedContext === context) {
    // Nothing to do. We already observe everything in this context.
  } else {
    const contextItem = {
      context: ((context: any): ReactContext<mixed>),
      memoizedValue: value,
      next: null,
    };
    if (lastContextDependency === null) {
      lastContextDependency = contextItem;
      consumer.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
      };
      if (enableLazyContextPropagation) {
        consumer.flags |= NeedsPropagation;
      }
    } else {
      // Append a new context item.
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return value;
}

只有被Provider组件包裹的子组件才能读取到Context的状态吗?

其实并不是,所有的组件都可以通过useContext去读取Context对象里的currentValue,但是,只有被Provider组件包裹的组件,才能订阅到Context对象里的value的变化,在变化的时候及时的更新自身组件的状态。这样设计的目的,实际上也是为了更好的优化React在更新组件的性能,试想,如果每创建一个Context对象,就默认所有的组件都可以订阅到这个Context的变化,那么整个Fiber树在更新的过程中,需要遍历的Fiber节点就太庞大了,一些完全不需要且没有订阅当前Context的组件也需要被遍历到,这其实是一种性能的浪费。

到此这篇关于一文带你深入理解React中的Context的文章就介绍到这了,更多相关React Context内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文