React のソースコードと原則の解釈 (16): Context と useContext

コラムの冒頭に書きました(スタックアーマー)

  1. 著者はフロントエンド技術の専門家ではなく、新しいことを学ぶのが好きなフロントエンド技術の初心者であり、急速に衰退するフロントエンド市場と、このコラムは著者が学習や経験の過程で考えたものであり、他のチュートリアルを参考にしている部分も多くありますが、誤りや問題があれば著者までご指摘ください。は 100% 正しいです。このコラムを参考回答として使用しないでください。

  2. このコラムを読むには、React、JavaScript、フロントエンドエンジニアリングのある程度の基礎が必要ですが、babel とは何か、jsx の構文とは何かなど、基本的な知識の多くは説明しませんので、参考にしてください。必要なときに資料を。

  3. このコラムの多くの部分は、他の多数のチュートリアルを参照しています。類似点がある場合は、作成者がそれらを盗用したものです。したがって、このチュートリアルは完全にオープン ソースです。作成者として、さまざまなチュートリアルを統合、要約し、独自の理解を追加することができます。

このセクションの内容

この章は、React ソース コード シリーズの最後の章です。主に、クロスレベル通信に使用する一般的なフック、つまり useContext について説明します。コンテキストの作成と使用から始めて、次にコンテキストの使用方法について説明します。 useContext のフックを使用して操作を簡素化し、最後にコンテキストの動作原理と実装プロセスを分析します。

コンテキストの定義

Context は、React によって公式に提供されているデータ転送メソッドであり、親コンポーネントがその下のコンポーネント ツリー全体にデータを提供できるようにします。

  • Props は、UI ツリーを使用するコンポーネントにデータを明示的に渡すための優れた方法です。しかし、コンポーネント ツリーの奥深くでパラメータを渡す必要があり、コンポーネント間で同じパラメータを再利用する必要がある場合、props を渡すのは面倒になります。最も近いルートの親コンポーネントは、データを必要とするコンポーネントから遠く離れている可能性があり、状態のプロモートが高すぎると、「レイヤーごとに props を渡す」状況が発生する可能性があります。

  • コンテキストの複数のレベル間では、必要なコンポーネントにデータを「直接」送信するための props を必要としないデータ転送方法です。

コンテキストの使用

React で Context を使用する手順は次のとおりです。

  1. コンテキストを作成します。
  2. データを指定するコンポーネントにこのコンテキストを指定します
  3. このコンテキストをサブコンポーネントで使用します

具体的な例を次に示します。

まず、createContextこのコンテキストを作成します。コンテキストは初期値を渡してコンテキストを返します。

const Context = React.createContext('default-value')

このコンテキストは、コンポーネントを Provider でラップすることで提供できます。その値は、サブコンポーネントの Provider の初期値です。以下は、Context でラップされたすべてのサブコンポーネントと同等です。Provider は、Context を通じて同じデータを取得できます。そのうちの初期値は新しい値です。

注: Provider で提供される値はコンテキストのデフォルト値であり、createContext で初期化される値はデフォルト値ではありません Provider がデフォルト値を提供しない場合に限り、定義時のデフォルト値が使用されます。

const Context = React.createContext('default-value')
function Parent() {
    
    
 return ( 
  // 在内部的后代组件都能够通过相同的 Ract.createContext() 的实例访问到 context 数据
  <Context.Provider value="new-value">
     <Children>
  <Context.Provider>
 )
}

また、サブコンポーネントはコンテキストを使用できます。ここでは、前にコンテキストを定義した位置からコンテキストをインポートする必要があります。同じデータを取得するには同じコンテキストのみを使用できます。

import Context from "xxxxxx"
<Context.Consumer>
      {
    
     v => {
    
    
        // 内部通过函数访问祖先组件提供的 Context 的值
        return <div> {
    
    v} </div>
      }}
</Context.Consumer>

プロバイダーによって提供される値を変更することで、コンテキストの内容を変更できます。例を次に示します。

  • 言語とその言語を変更するメソッドをコンテキストとしてサブコンポーネントに渡します。
  • サブコンポーネントはコンシューマを通じてこれらのコンテンツを受け取り、ボタンをクリックしてプロバイダが提供するメソッドを呼び出して言語を変更します。
  • 注意すべき点は、 Context.Provider値が変更されると、Context を使用するすべてのコンポーネントが強制的に更新されることです。
class App extends Component {
    
    
  setLanguage = language => {
    
    
    this.setState({
    
     language });
  };

  state = {
    
    
    language: "en",
    setLanguage: this.setLanguage
  };
    
  render() {
    
    
    return (
      <LanguageContext.Provider value={
    
    this.state}>
        <h2>Current Language: {
    
    this.state.language}</h2>
        <p>Click button to change to jp</p>
        <div>
          <LanguageSwitcher />
        </div>
      </LanguageContext.Provider>
    );
  }
}

class LanguageSwitcher extends Component {
    
    
  render() {
    
    
    return (
      <LanguageContext.Consumer>
        {
    
    ({
     
      language, setLanguage }) => (
          <button onClick={
    
    () => setLanguage("jp")}>
            Switch Language (Current: {
    
    language})
          </button>
        )}
      </LanguageContext.Consumer>
    );
  }
}

useContext の定義と使用法

useContextコンポーネント内のコンテキストを読み取り、サブスクライブできるようにする React フックです。

const value = useContext(SomeContext)

これにより、Context を消費するプロセスが実際に簡素化され、Consumer を通じて必要なデータを取得する必要がなくなりましたが、useContext を通じて Context の内部値を取得できます。

import Context from "xxxxxx"
function Child() {
    
    
  const {
    
     ctx } = useContext(Context)
  return <div> {
    
    ctx} </div>
}

コンテキストの使用シナリオ

  • コンテキストは、ログインステータス管理ナイトモードなど、複数のサブコンポーネントやサブコンポーネントのサブコンポーネントに特定の状態を提供する必要がある場合によく使用されます。
  • Context は、祖父母と孫の間でパラメータを渡すためにも便利に使用できます。コンポーネントの孫が祖父母の状態を使用する必要がある場合、通常の状況では、複数の props 層を通過する必要がありますが、Context を使用すると、そのような複雑さを回避できます。プロセス

コンテキストソースコード

コンテキストの作成

コンテキストの原理に関しては、packages/shared/ReactTypes.jsファイルにあるコンテキスト クラスの定義から始めましょう。

export type ReactContext<T> = {
    
    
  $$typeof: Symbol | number,
  Consumer: ReactContext<T>,           // 消费 context 的组件
  Provider: ReactProviderType<T>,      // 提供 context 的组件
  // 保存 2 个 value 用于支持多个渲染器并发渲染
  _currentValue: T,
  _currentValue2: T,
  _threadCount: number, // 用来追踪 context 的并发渲染器数量
  // DEV only
  _currentRenderer?: Object | null,
  _currentRenderer2?: Object | null,
    
  displayName?: string,  // 别名
  _defaultValue: T,      
  _globalName: string,
  ...
};

createContext は、ユーザーが使用するデータ、コンシューマー、プロバイダーを含む新しいデータ構造を作成するもので、そのコードはファイル package/react/src/ReactContext.js にあります。

export function createContext<T>(defaultValue: T): ReactContext<T> {
    
    
    
  const context: ReactContext<T> = {
    
    
    $$typeof: REACT_CONTEXT_TYPE,   // 用 $$typeof 来标识这是一个 context
    _currentValue: defaultValue,    // 给予初始值
    _currentValue2: defaultValue,   // 给予初始值
    _threadCount: 0,
    Provider: (null: any),
    Consumer: (null: any),
    _defaultValue: (null: any),
    _globalName: (null: any),
  };
	
  // 添加 Provider ,并且 Provider 中的_context指向的是 context 对象
  context.Provider = {
    
    
    $$typeof: REACT_PROVIDER_TYPE,   // 用 $$typeof 来标识这是一个 Provider 的 symbol
    _context: context,
  };

  let hasWarnedAboutUsingNestedContextConsumers = false;
  let hasWarnedAboutUsingConsumerProvider = false;
  let hasWarnedAboutDisplayNameOnConsumer = false;
	
  // 添加 Consumer
  context.Consumer = context;
  return context;
}

コンテキストの提供

Context を作成した後、次のステップは Context を提供することです。Context は Provider を使用して Context コンテンツを提供することがわかり、<Context.Provider>これは DOM 要素ノードとして jsx コードに書き込まれます値は何ですか? 子要素には何が提供されますか? 、 を見ようよ:

上で述べたように、プロバイダーを識別する$$typeof ために。前のチュートリアルを読んだ読者はこれに精通しているはずです。最初のチュートリアルで述べたように、このフィールドを ReactElement で使用して、これが React. 要素であることを識別します。同様に、ここでもこのフィールドを使用して Provider 要素を識別し、Fibre 生成時に統一処理を実行できるようにします。

コードは /packages/react-reconciler/src/ReactFiber.old.js 関数にあり、createFiberFromTypeAndProps を呼び出して Fiber を作成します。この中で渡された型によって判断し、$$typeof の値に従って Fiber を与えます。タグはさまざまな値を追加します。上記では、コンテキストを作成するときに、プロバイダーは REACT_PROVIDER_TYPE 型を指定し、コンシューマーはコンテキスト自体を指すため、REACT_CONTEXT_TYPE 型フィールドになります。したがって、これら 2 つの型を jsx で解析すると、それは、対応するフィールドとして決定されます:

export function createFiberFromTypeAndProps(
  type: any, // React$ElementType,element的类型
  key: null | string,
  pendingProps: any,
  owner: null | Fiber,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
    
    
  let fiberTag = IndeterminateComponent;
  let resolvedType = type;
  if (typeof type === 'function') {
    
    
	// ....
  } else if (typeof type === 'string') {
    
    
	// ....
  } else {
    
    
    getTag: switch (type) {
    
    
	// ....
      default: {
    
    
        if (typeof type === 'object' && type !== null) {
    
    
          switch (type.$$typeof) {
    
    
            case REACT_PROVIDER_TYPE:
              fiberTag = ContextProvider;
              break getTag;
            case REACT_CONTEXT_TYPE:
              fiberTag = ContextConsumer;
              break getTag;
			//.....
          }
        }
      }
    }
  }

対応するタイプを決定した後、引き続き Fiber の処理を​​見ていきます:関数beginWorkで、さまざまなタグを処理します。まず ContextProvider の処理を​​見てみましょう。

  • まず、現在受信している pendingProps (渡した props) を取得し、次に渡した値を取得します。
  • 次に、pushProvider 関数を呼び出します。この関数は、コンテキストの _currentValue を変更します。つまり、コンテキストの値を更新します。
  • PushProvider 関数はプッシュ操作も実行します。これについては後で詳しく説明します。
  • その後、再利用可能かどうかを判断します(これについては以前に詳しく説明しました)。
  • 再利用できない場合は、propagateContextChange メソッドを通じて更新をマークする必要があります。
function updateContextProvider(current, workInProgress, renderLanes) {
    
    
  const providerType = workInProgress.type
  const context = providerType._context
  const newProps = workInProgress.pendingProps
  const oldProps = workInProgress.memoizedProps
  const newValue = newProps.value
  pushProvider(workInProgress, context, newValue)
  // 是更新
  if (oldProps !== null) {
    
    
    const oldValue = oldProps.value
    // 可以复用
    if (is(oldValue, newValue)) {
    
    
      if (
        oldProps.children === newProps.children &&
        !hasLegacyContextChanged()
      ) {
    
    
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes,
        )
      }
    } else {
    
    
      // 查找 consumer 消费组件,标记更新
      propagateContextChange(workInProgress, context, renderLanes)
    }
  }
  // 继续遍历
  const newChildren = newProps.children
  reconcileChildren(current, workInProgress, newChildren, renderLanes)
  return workInProgress.child
}

function pushProvider(providerFiber, context, nextValue) {
    
    
  // 压栈
  push(valueCursor, context._currentValue, providerFiber)
  // 修改 context 的值
  context._currentValue = nextValue
}

function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {
    
    
  index++;
  valueStack[index] = cursor.current;
  cursor.current = value;
}

コンテキストを更新する必要がある場合、propagateContextChange メソッドを呼び出して更新をマークすることがわかりました。では、その具体的なロジックは何でしょうか? これは主に関数 propagateContextChange_eager を呼び出します。この関数を見てみましょう。

深さ優先はすべての子孫ファイバーを走査し、要素が依存するすべてのコンテキストをマウントするdependencies内の。そのマウントについては次のセクションで説明します。dependenciesコンテキストと現在のプロバイダーを比較します。コンテキストが同じ; 同じ場合は、this.forceUpdate を呼び出して行われる更新と同様に、ファイバー更新の優先順位が高い更新が作成されます。

function propagateContextChange_eager<T>(
  workInProgress: Fiber,
  context: ReactContext<T>,
  renderLanes: Lanes,
): void {
    
    
  let fiber = workInProgress.child;
  if (fiber !== null) {
    
    
    fiber.return = workInProgress;
  }
  // 深度优先遍历整个 fiber 树
  while (fiber !== null) {
    
    
    let nextFiber;
    const list = fiber.dependencies;
    if (list !== null) {
    
    
      nextFiber = fiber.child;
      let dependency = list.firstContext;
      // 获取 dependencies
      while (dependency !== null) {
    
    
        // 如果是同一个 context
        if (dependency.context === context) {
    
    
          if (fiber.tag === ClassComponent) {
    
    
            const lane = pickArbitraryLane(renderLanes);
            const update = createUpdate(NoTimestamp, lane);
            // 高优先级,强制更新
            update.tag = ForceUpdate;
            const updateQueue = fiber.updateQueue;
            if (updateQueue === null) {
    
    
            } else {
    
    
              const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
              const pending = sharedQueue.pending;
              if (pending === null) {
    
    
                update.next = update;
              } else {
    
    
                update.next = pending.next;
                pending.next = update;
              }
              sharedQueue.pending = update;
            }
          }
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if (alternate !== null) {
    
    
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          scheduleContextWorkOnParentPath(
            fiber.return,
            renderLanes,
            workInProgress,
          );
          list.lanes = mergeLanes(list.lanes, renderLanes);
          break;
        }
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
    
    
      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
    } else if (fiber.tag === DehydratedFragment) {
    
    
      //... 省略
    } else {
    
    
      nextFiber = fiber.child;
    }
    // 深度优先遍历找到下一个节点
    if (nextFiber !== null) {
    
    
      nextFiber.return = fiber;
    } else {
    
    
      nextFiber = fiber;
      while (nextFiber !== null) {
    
    
        if (nextFiber === workInProgress) {
    
    
          nextFiber = null;
          break;
        }
        const sibling = nextFiber.sibling;
        if (sibling !== null) {
    
    
          sibling.return = nextFiber.return;
          nextFiber = sibling;
          break;
        }
        nextFiber = nextFiber.return;
      }
    }
    fiber = nextFiber;
  }
}

消費コンテキスト

上では<Context.Provider>ノード、次にさまざまな消費方法のソース コード処理方法について説明します。

まず、最も一般的に使用されるメソッドは Context.Consumer です。その処理はまだbeginWork関数内。前の部分で説明したように、Consumer はコンテキスト自体を指すため、REACT_CONTEXT_TYPE 型フィールドになります。ファイバーを生成するとき、それはREACT_CONTEXT_TYPE タイプを認識し、 ContextConsumer タグを追加します。このタグを認識すると、処理のために updateContextConsumer を呼び出します。

updateContextConsumer のロジックは、最初に prepareToReadContext と readContext を通じて最新のコンテキスト値を取得し、次に更新操作のために最新の値をサブコンポーネントに渡すことです。

function beginWork(current, workInProgress, renderLanes) {
    
    
  switch (workInProgress.tag) {
    
    
    case ContextConsumer:
      return updateContextConsumer(current, workInProgress, renderLanes)
  }
}

function updateContextConsumer(current, workInProgress, renderLanes) {
    
    
  let context = workInProgress.type
  context = context._context
  const newProps = workInProgress.pendingProps
  const render = newProps.children
  // 准备读取 context
  prepareToReadContext(workInProgress, renderLanes)
  // 获取最新的 context
  const newValue = readContext(context)
  // 更新包裹的子组件
  let newChildren
  newChildren = render(newValue)
  reconcileChildren(current, workInProgress, newChildren, renderLanes)
  return workInProgress.child
}

prepareToReadContext では、currentlyRenderingFiber を現在のノードとして設定します。これは、後続のアクセスに便利です。現在のノードに依存関係のリンク リストがない場合は、コンテキスト要素をマウントするために使用されるリンク リストを初期化します。

readContext では、コンポーネントが依存するさまざまなコンテキストをすべて収集し、fiber.dependenciesリンクされたリスト必要な値として context._currentValue を返します。生成された依存関係は、後で context を更新するときに使用されます。上で述べたように

function prepareToReadContext(workInProgress, renderLanes) {
    
    
  currentlyRenderingFiber = workInProgress
  lastContextDependency = null
  lastFullyObservedContext = null   // 重置
  const dependencies = workInProgress.dependencies
  if (dependencies !== null) {
    
    
    const firstContext = dependencies.firstContext
    if (firstContext !== null) {
    
    
      if (includesSomeLane(dependencies.lanes, renderLanes)) {
    
    
        markWorkInProgressReceivedUpdate()
      }
      dependencies.firstContext = null
    }
  }
}
export function readContext<T>(context: ReactContext<T>): T {
    
    
    
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  if (lastFullyObservedContext === context) {
    
    
      // 不是可以使用 context 的时机
  } else {
    
    
    const contextItem = {
    
    
      context: ((context: any): ReactContext<mixed>),
      memoizedValue: value,
      next: null,
    };
    if (lastContextDependency === null) {
    
    

      lastContextDependency = contextItem;
      currentlyRenderingFiber.dependencies = {
    
    
        lanes: NoLanes,
        firstContext: contextItem,
      };
    } else {
    
    
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return value;
}

useContext

useContext は関数コンポーネントのフックとして使用され、その機能は基本的に上記の直接使用と同じですが、関数の位置がフックの関連関数に変更されます。と が実際に上記の関数である readContext 関数を呼び出しているuseContextことがわかります。OnMountOnUpdate

const HooksDispatcherOnMount: Dispatcher = 
  useContext: readContext,
  //....
};

const HooksDispatcherOnUpdate: Dispatcher = {
    
    
  useContext: readContext,
  //....
};

コンテキストを破壊する

最後に、コミット ステージを見てみましょう。このステージのcompleteWork関数、関数を呼び出しますpopProvider。関数は以前の関数pushProviderをエコーしスタック内の要素をスローします。

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

  {
    
    
    context._currentValue = currentValue;
  }
}

function pop<T>(cursor: StackCursor<T>, fiber: Fiber): void {
    
    
  if (index < 0) {
    
    
    return;
  }
  cursor.current = valueStack[index];
  valueStack[index] = null;
  index--;
}

次に、コンテキストを保存するためにこのスタックを使用する理由を分析しましょう。

次のようなコードがあるとします。コンテキストはクロスレベルであるため、深さ優先トラバーサル中にコンポーネントでコンテキスト値を渡す必要がある場合は、それをレイヤーごとに渡す必要があるため、追加のオーバーヘッドは許容できないため、グローバル変数を使用して記録します。ただし、プロバイダーは入れ子になっている可能性があり、コード内に異なる値を持つ複数のプロバイダーが存在することになります。

深さ優先検索を使用してファイバーを横断するだけで、これは先入れ後出しの配信プロセスであるため、スタックを使用してすべてのコンテキストを記録し、プロバイダーを読み取るときに値をスタックに入れることができます。別のプロバイダーがその中にネストされている場合、このネストされたプロバイダーの子ノードがそれに最も近い新しい値を取得できるように、スタックにビットを追加して値を更新します。このプロバイダーが破棄されると、この値がスタックからスローされ、上位レベルのプロバイダーによってラップされた他のサブコンポーネントが上位レベルのプロバイダーの値を取得します。

render() {
    
    
  return (
    <>
      <TestContext.Provider value={
    
    10}>
        <Test1 />
        <TestContext.ProviderProvider value={
    
    100}>
          <Test2 />
        </TestContext.Provider>
      </TestContext.Provider>
    </>
  )
}

上記の原則は、Context.Providerの値が変更されると、入れ子になったプロバイダーが関与する可能性があるため、Context.Providerの値が変更されると、 Contextを使用するすべてのコンポーネントが更新を強制される理由の 1 つの点も説明しています。ローカルで更新すると、スタックを更新するのは正常ではなくなります。呼び出し順序が変わらないようにスタック全体を破棄してから再生成する必要があるため、ラップするすべてのサブ要素を再レンダリングする必要があります。

要約する

上記はコンテキストに関連する内容ですが、要約してみましょう。

  • React では、Context を使用してコンテキストを作成し、データを指定するコンポーネントでこのコンテキストを提供し、子コンポーネントこのコンテキストを使用する必要があります。
  • ユーザーはコンテキストcreateContextを作成し、データ、コンシューマとプロバイダを初期化し、それらが同じ ReactContext オブジェクトを指すようにして、ユーザーが常に最新のコンテキストを取得できるように_currentValue します。ReactContext のプロパティには、このコンテキストのデータが含まれます
  • $$typeof を使用してコンポーネントを Consumer または Provider として識別します。これらは、reactElement オブジェクトとして処理され、 Fibre の生成時にそれらを識別するために別のタグが使用されます。
  • プロバイダーが初期化されると、コンテキストの値をbeginWorkのスタックにプッシュします。
  • Consumer が初期化されると、Fiber が依存するすべてのコンテキストがdependenciesリンク リストは ReactContext 自体を指すため、_currentValue を通じて必要なオブジェクトを直接取得できます。
  • コンテキストが更新されると、プロバイダーが判断を行います。値が変更され、再利用できない場合は、すべての子ノードpropagateContextChangeを走査するために。このプロバイダーを使用するノードは、必須の更新優先度としてマークされ、更新されます。その後のプロセス。
  • コミットフェーズでプロバイダーが処理されると、スタックにプッシュされた値がポップアウトされ対応するコンテキスト値もスタック内の前のノードのコンテンツに更新されます。複数のネストされたコンテキスト。ユーザーは常に最も近いプロバイダーから提供される値を取得します。
  • useContext自体はフックとして、関数コンポーネントを適応させるだけであり、コンシューマ ロジックで readContext 関数を呼び出してコンテキストの値を取得します。

これまで、React のソースコードチュートリアルをセクションごとに更新してきましたが、内容が複雑で、読んで理解するのに多くの時間がかかりました。その後、時間を見つけて、要約と合理的な記事を書いてこれを終了します。チュートリアル. ここまで読んでくださった皆様、ありがとうございました。

おすすめ

転載: blog.csdn.net/weixin_46463785/article/details/131260862