Reactイベントメカニズムソースコード分析

Reactイベントメカニズムソースコード分析

React v17 ではイベントの仕組みが大きく変更されており、v16 とはかなり異なっていると思います。

この記事で分析した React のバージョンは 17.0.1 であり、アプリケーションは優先度関連の情報なしで ReactDOM.render を使用して作成されています。

原理

React のイベントは、委任イベント (DelegatedEvent) と非委任イベント (NonDelegatedEvent) に分けられます。委任イベントが fiberRoot によって作成されると、ほぼすべてのイベント処理関数がルート ノードの DOM 要素にバインドされますが、非委任イベントでは、処理関数が DOM 要素自体にのみバインドされます。

同時に、React はイベントを discreteEvent、userBlockingEvent、continuousEvent の 3 つのタイプに分類します。これらは優先順位が異なり、イベント処理関数をバインドするときに異なるコールバック関数を使用します。

React イベントはネイティブ ベースで構築され、一連のバブリングとキャプチャ イベント メカニズムをシミュレートします。DOM 要素がイベントをトリガーすると、React のルート ノードにバインドされている処理関数にバブリングし、ターゲットを通じてイベントをトリガーした DOM オブジェクトと対応する Fiber ノードを取得します。Fiber ノードは上位の親にトラバースしてイベント キューを収集し、キューをトラバースしてキュー内の各 Fiber オブジェクトに対応するイベント処理関数をトリガーします。前方トラバーサルはバブリングをシミュレートし、後方トラバーサルはキャプチャをシミュレートするため、合成イベントはネイティブ イベントの後にトリガーされます。

Fiber オブジェクトに対応するイベント処理関数は、引き続き props に格納されます。コレクションは props から取得されるだけで、どの要素にもバインドされません。

ソースコード分析

以下のソース コードは、イベント メカニズムのトリガー プロセスを明確にし、プロセスに関係のない複雑なコードを大量に削除することを目的とした、基本ロジックの簡単な分析のみです。

委任されたイベントバインディング

このステップは、ReactDOM.render が呼び出されたときに発生します。fiberRoot が作成されると、サポートされているすべてのイベントがルート ノードの DOM 要素でリッスンされます。

関数createRootImpl(
  コンテナ: コンテナ、
  タグ: ルートタグ、
  オプション: void | RootOptions,
){
  // ...
  定数ルートコンテナ要素 =
        container.nodeType === COMMENT_NODE ? container.parentNode : コンテナ;
  // サポートされているすべてのイベントをリッスンします listenToAllSupportedEvents(rootContainerElement);
  // ...
}

すべてのサポートされているイベントを聴く

イベントをバインドすると、対応する eventName は allNativeEvents という名前の Set 変数を通じて取得されます。この変数はトップレベル関数で収集され、nonDelegatedEvents は定義済みの Set です。

エクスポート関数 listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  allNativeEvents.forEach(domEventName => {
    // 委任を必要としないイベントを除外する if (!nonDelegatedEvents.has(domEventName)) {
      // バブル listenToNativeEvent(
        domイベント名、
        間違い、
        ((rootContainerElement: 任意): 要素),
        ヌル、
      );
    }
    // listenToNativeEvent をキャプチャする(
      domイベント名、
      真実、
      ((rootContainerElement: 任意): 要素),
      ヌル、
    );
  });
}

ネイティブイベントを聴く

listenToNativeEvent 関数は、イベントをバインドする前に DOM 要素にイベント名をマークし、false と判断された場合にのみバインドします。

エクスポート関数 listenToNativeEvent(
  DOMイベント名: DOMイベント名、
  isCapturePhaseListener: ブール値、
  ルートコンテナ要素: イベントターゲット、
  targetElement: 要素 | null、
  イベントシステムフラグ?: イベントシステムフラグ = 0、
): 空所 {
  ターゲットを rootContainerElement とします。
	// ...
  // 現在の要素がリッスンするイベントを識別するために、DOM 要素にセットを格納します。const listenerSet = getEventListenerSet(target);
  // イベント識別キー、文字列連結処理 const listenerSetKey = getListenerSetKey(
    domイベント名、
    キャプチャフェーズリスナー、
  );

  リスナーセットにリスナーセットキーがある場合
    // キャプチャとしてマーク if (isCapturePhaseListener) {
      イベントシステムフラグ |= IS_CAPTURE_PHASE;
    }
    // イベントをバインド addTrappedEventListener(
      ターゲット、
      domイベント名、
      イベントシステムフラグ、
      キャプチャフェーズリスナー、
    );
    // セットに追加
    リスナーセット。リスナーセットキーを追加します。
  }
}

トラップイベントリスナーを追加

addTrappedEventListener 関数は、イベント名を通じて対応する優先度のリスナー関数を取得し、それを下位レベルの関数に渡してイベント バインディングを処理します。

このリスナー関数はクロージャー関数であり、targetContainer、domEventName、eventSystemFlags の 3 つの変数にアクセスできます。

関数 addTrappedEventListener(
  ターゲットコンテナ: イベントターゲット、
  DOMイベント名: DOMイベント名、
  イベントシステムフラグ: イベントシステムフラグ、
  isCapturePhaseListener: ブール値、
  isDeferredListenerForLegacyFBSupport?: ブール値、
){
  // 優先度に応じて対応するリスナーを取得します
  リスナー = createEventListenerWrapperWithPriority(
    ターゲットコンテナ、
    domイベント名、
    イベントシステムフラグ、
  );

  if (isCapturePhaseListener) {
    イベントキャプチャリスナーを追加します(ターゲットコンテナ、domEventName、リスナー)。
  } それ以外 {
    ターゲットコンテナ、domEventName、リスナーにEventBubbleListenerを追加します。
  }
}

addEventCaptureListener 関数と addEventBubbleListener 関数は、ネイティブの target.addEventListener を呼び出してイベントをバインドします。

このステップでは、イベント名を含むセットをループし、各イベントに対応する処理関数をルート ノードの DOM 要素にバインドします。

デリゲートイベントバインディングは不要

委任を必要としないイベントには、メディア要素のイベントも含まれます。

エクスポートconst nonDelegatedEvents: Set<DOMEventName> = new Set([
  'キャンセル'、
  '近い'、
  '無効'、
  '負荷'、
  'スクロール'、
  'トグル'、
  ...メディアイベントタイプ、
]);
エクスポートconst mediaEventTypes: Array<DOMEventName> = [
  'アボート'、
  「プレイできる」、
  'プレイスルー可能'、
  '期間変更'、
  「空になった」
  「暗号化」、
  「終了」、
  'エラー'、
  'ロードされたデータ'、
  'ロードされたメタデータ'、
  'ロードスタート'、
  '一時停止'、
  '遊ぶ'、
  「遊ぶ」、
  '進捗'、
  'レート変更'、
  「求めた」、
  「求めている」、
  「行き詰まった」、
  'つるす'、
  'timeupdate'、
  'ボリューム変更'、
  '待っている'、
];

初期プロパティの設定

setInitialProperties メソッドは、委任なしで DOM 要素自体に直接バインドし、スタイルといくつかの受信 DOM 属性も設定します。

エクスポート関数setInitialProperties(
  domElement: 要素、
  タグ: 文字列、
  rawProps: オブジェクト、
  rootContainerElement: 要素 | ドキュメント、
): 空所 {
  プロパティを設定します: オブジェクト;
  スイッチ (タグ) {
    // ...
    ケース「ビデオ」:
    ケース 'オーディオ':
      (i = 0 とします; i < mediaEventTypes.length; i++) {
        DOM要素のmediaEventTypes[i]をlistenToNonDelegatedEventとして取得します。
      }
      プロパティ = 生のプロパティ;
      壊す;
    デフォルト:
      プロパティ = 生のプロパティ;
  }
  // スタイルなどの DOM 属性を設定します...
  初期DOMプロパティを設定する(
    タグ、
    dom要素、
    ルートコンテナ要素、
    小道具、
    isCustomComponentTag、
  );
}

スイッチでは、対応するイベントがさまざまな要素タイプに応じてバインドされます。ここでは、ビデオ要素とオーディオ要素の処理のみが残っています。これらは mediaEventTypes をトラバースして、イベントを DOM 要素自体にバインドします。

非委任イベントを聴く

listenToNonDelegatedEvent メソッドのロジックは、基本的に前のセクションの listenToNativeEvent メソッドと同じです。

エクスポート関数 listenToNonDelegatedEvent(
  DOMイベント名: DOMイベント名、
  targetElement: 要素、
): 空所 {
  定数isCapturePhaseListener = false;
  リスナーセットは、ターゲット要素のイベントリスナーセットを取得します。
  定数リスナーSetKey = getListenerSetKey(
    domイベント名、
    キャプチャフェーズリスナー、
  );
  リスナーセットにリスナーセットキーがある場合
    トラップイベントリスナーを追加します(
      ターゲット要素、
      domイベント名、
      IS_NON_DELEGATED、
      キャプチャフェーズリスナー、
    );
    リスナーセット。リスナーセットキーを追加します。
  }
}

イベント処理は DOM 要素自体にバインドされていますが、バインドされたイベント処理関数はコードで渡される関数ではなく、後続のトリガーは引き続き実行のために処理関数を収集することに注意してください。

イベントハンドラ

イベント処理関数は、コードで渡される関数ではなく、React のデフォルトの処理関数を参照します。

この関数は createEventListenerWrapperWithPriority メソッドによって作成され、対応する手順は上記の addTrappedEventListener にあります。

優先度付きイベントリスナーラッパーを作成する

関数createEventListenerWrapperWithPriority(をエクスポートする
  ターゲットコンテナ: イベントターゲット、
  DOMイベント名: DOMイベント名、
  イベントシステムフラグ: イベントシステムフラグ、
): 関数 {
  // 組み込みマップからイベントの優先度を取得します。const eventPriority = getEventPriorityForPluginSystem(domEventName);
  リスナーラッパーを作成します。
  // 優先度に応じて異なるリスナーを返す
  スイッチ (イベント優先度) {
    離散イベントの場合:
      リスナーラッパー = dispatchDiscreteEvent;
      壊す;
    UserBlockingEventの場合:
      リスナーラッパー = ディスパッチユーザーブロック更新;
      壊す;
    連続イベントの場合:
    デフォルト:
      リスナーラッパー = ディスパッチイベント;
      壊す;
  }
  listenerWrapper.bind() を返します。
    ヌル、
    domイベント名、
    イベントシステムフラグ、
    ターゲットコンテナ、
  );
}

createEventListenerWrapperWithPriority 関数は、イベントの優先度に対応するリスナーを返します。これら 3 つの関数はすべて 4 つのパラメータを受け取ります。

関数fn(
  domイベント名、
  イベントシステムフラグ、
  容器、
  ネイティブイベント、
){
  //...
}

戻るときに、bind は 3 つのパラメータで渡されるため、返される関数は nativeEvent のみを受け取り、最初の 3 つのパラメータにアクセスできる処理関数になります。

実際には、dispatchEvent メソッドは、dispatchDiscreteEvent メソッドと dispatchUserBlockingUpdate メソッドによって内部的に呼び出されます。

ディスパッチイベント

ここでは多くのコードが削除されており、イベントをトリガーするコードのみが表示されています。

エクスポート関数dispatchEvent(
  DOMイベント名: DOMイベント名、
  イベントシステムフラグ: イベントシステムフラグ、
  ターゲットコンテナ: イベントターゲット、
  ネイティブイベント: 任意のネイティブイベント、
): 空所 {
  // ...
  // イベントをトリガーする attemptToDispatchEvent(
    domイベント名、
    イベントシステムフラグ、
    ターゲットコンテナ、
    ネイティブイベント、
  );
  // ...
}

attemptToDispatchEvent メソッドは依然として多くの複雑なロジックを処理し、関数呼び出しスタックには複数のレイヤーがあります。これらはすべてスキップし、主要なトリガー関数のみを見ていきます。

プラグインのディスパッチイベント

dispatchEventsForPlugins 関数は、イベントをトリガーする各レベルのノードに対応する処理関数、つまり実際に JSX に渡す関数を収集して実行します。

関数dispatchEventsForPlugins(
  DOMイベント名: DOMイベント名、
  イベントシステムフラグ: イベントシステムフラグ、
  ネイティブイベント: 任意のネイティブイベント、
  targetInst: null | ファイバー、
  ターゲットコンテナ: イベントターゲット、
): 空所 {
  ネイティブイベントターゲットを取得します。
  定数ディスパッチキュー: ディスパッチキュー = [];
  // バブリングをシミュレートするためのリスナーを収集する extractEvents(
    ディスパッチキュー、
    domイベント名、
    ターゲットInst、
    ネイティブイベント、
    ネイティブイベントターゲット、
    イベントシステムフラグ、
    ターゲットコンテナ、
  );
  //実行キュー processDispatchQueue(dispatchQueue, eventSystemFlags);
}

抽出イベント

extractEvents 関数は主に、さまざまな種類のイベントに対応する合成イベントを作成し、各レベルのノードのリスナーを収集して、バブリングまたはキャプチャをシミュレートします。

ここのコードは長くなっており、多くの無関係なコードが削除されています。

関数extractEvents(
  ディスパッチキュー: ディスパッチキュー、
  DOMイベント名: DOMイベント名、
  targetInst: null | ファイバー、
  ネイティブイベント: 任意のネイティブイベント、
  ネイティブイベントターゲット: null | イベントターゲット、
  イベントシステムフラグ: イベントシステムフラグ、
  ターゲットコンテナ: イベントターゲット、
): 空所 {
  const reactName = topLevelEventsToReactNames.get(domEventName);
  SyntheticEventCtor を SyntheticEvent とします。
  reactEventType: string = domEventName; とします。
	// 異なるイベントに応じて異なる合成イベントを作成する switch (domEventName) {
    ケース 'キー押下':
    ケース 'keydown':
    ケース 'keyup':
      SyntheticEventCtor = SyntheticKeyboardEvent;
      壊す;
    'クリック'の場合:
    // ...
    'マウスオーバー'の場合:
      SyntheticEventCtor = SyntheticMouseEvent;
      壊す;
    ケース「ドラッグ」:
    // ...
    ケース「ドロップ」:
      SyntheticEventCtor = SyntheticDragEvent;
      壊す;
    // ...
    デフォルト:
      壊す;
  }
  // ...
  // 各レベルでリスナーを収集する
  const リスナー = シングルフェーズリスナーを蓄積する(
    ターゲットInst、
    反応名、
    ネイティブイベントタイプ、
    キャプチャフェーズ中、
    累積ターゲットのみ、
  );
  リスナーの長さが0より大きい場合
    // 合成イベントを作成する const event = new SyntheticEventCtor(
      反応名、
      反応イベントタイプ、
      ヌル、
      ネイティブイベント、
      ネイティブイベントターゲット、
    );
    ディスパッチキューをプッシュします({イベント、リスナー});
  }
}

シングルフェーズリスナーを蓄積する

assembleSinglePhaseListeners 関数は上位層を走査して、後でバブリングをシミュレートするために使用されるリストを収集します。

エクスポート関数accumulateSinglePhaseListeners(
  targetFiber: ファイバー | null、
  reactName: 文字列 | null、
  ネイティブイベントタイプ: 文字列、
  inCapturePhase: ブール値、
  累積ターゲットのみ: ブール値、
): 配列<DispatchListener> {
  const captureName = reactName !== null ? reactName + 'Capture' : null;
  const reactEventName = inCapturePhase ? captureName: reactName;
  const リスナー: Array<DispatchListener> = [];

  インスタンスを targetFiber にします。
  lastHostComponent を null にします。

  // イベントをトリガーするファイバーノードをトラバースしてDOMとリスナーを収集します
  while (インスタンス !== null) {
    const {stateNode, タグ} = インスタンス;
    // HostComponents のみにリスナーがあります (つまり <div>)
    if (タグ === HostComponent && stateNode !== null) {
      最後のホストコンポーネント = stateNode;

      (reactEventName !== null)の場合{
        // ファイバー ノードの props から受信イベント リスナー関数を取得します。const listener = getListener(instance, reactEventName);
        if (リスナー != null) {
          リスナー.push({
            実例、
            リスナー、
            現在のターゲット: lastHostComponent、
          });
        }
      }
    }
    ターゲットのみを累積する場合
      壊す;
    }
    // 上へ進むinstance = instance.return;
  }
  リスナーを返します。
}

最終的なデータ構造は次のようになります。

dispatchQueue のデータ構造は、[{event,listeners }] 型の配列です。

リスナーは、タイプ [{ currentTarget, instance, listener }] のレイヤーごとに収集されたデータです。

プロセスディスパッチキュー

dispatchQueue は processDispatchQueue 関数で走査されます。

エクスポート関数 processDispatchQueue(
  ディスパッチキュー: ディスパッチキュー、
  イベントシステムフラグ: イベントシステムフラグ、
): 空所 {
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  (i = 0 とします; i < ディスパッチキューの長さ; i++) {
    const {イベント、リスナー} = dispatchQueue[i];
    processDispatchQueueItemsInOrder(イベント、リスナー、inCapturePhase);
  }
}

dispatchQueue 内の各項目は、processDispatchQueueItemsInOrder 関数で走査され、実行されます。

プロセスディスパッチキューアイテムの順序

関数 processDispatchQueueItemsInOrder(
  イベント: ReactSyntheticEvent、
  ディスパッチリスナー: 配列<ディスパッチリスナー>,
  inCapturePhase: ブール値、
): 空所 {
  前のインスタンスを作成します。
  // キャプチャ if (inCapturePhase) {
    (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {インスタンス、現在のターゲット、リスナー} = dispatchListeners[i];
      if (インスタンス !== previousInstance && event.isPropagationStopped()) {
        戻る;
      }
      executeDispatch(イベント、リスナー、現在のターゲット);
      前のインスタンス = インスタンス;
    }
  } それ以外 {
  // バブル for (let i = 0; i < dispatchListeners.length; i++) {
      const {インスタンス、現在のターゲット、リスナー} = dispatchListeners[i];
      if (インスタンス !== previousInstance && event.isPropagationStopped()) {
        戻る;
      }
      executeDispatch(イベント、リスナー、現在のターゲット);
      前のインスタンス = インスタンス;
    }
  }
}

processDispatchQueueItemsInOrder 関数は、判断に応じて前方および後方に移動することで、バブリングとキャプチャをシミュレートします。

ディスパッチ実行

リスナーは executeDispatch 関数内で実行されます。

関数executeDispatch(
  イベント: ReactSyntheticEvent、
  リスナー: 関数、
  現在のターゲット: イベントターゲット、
): 空所 {
  const type = event.type || '不明なイベント';
  イベントの現在のターゲット = 現在のターゲット;
  リスナー(イベント);
  イベント.currentTarget = null;
}

結論

この記事は、イベントメカニズムの実行を明らかにすることを目的としています。関数実行スタックに従ってコードロジックを単純にリストします。コードを比較しないと理解するのは困難です。原理は最初に説明します。

React のイベントの仕組みはわかりにくく複雑です。さまざまな状況に基づいて多くの判断を行い、優先度関連のコードや合成イベントもあります。ここではそれらを一つ一つ説明していません。もちろん、まだ読んでいないからです。

私は通常、React を使用して簡単なモバイル ページを作成します。上司は読み込み速度が十分でないと文句を言っていましたが、私にできることは何もありませんでした。私の仕事に関する限り、Cocurrent があるかどうかは問題ではありませんでした。合成イベントはより複雑で、まったく不要でした。ただし、React の作者は非常に創造的です。ソース コードを読んでいなければ、一連のイベント メカニズムをシミュレートしているとは思いもしませんでした。

小さな思い

  • ネイティブ イベントの stopPropagation によって合成イベントの配信が防止されるのはなぜですか?

これまでこれらの疑問について考えたことはなかったのですが、今日ソースコードを読んだ後に考えてみました。

  • 合成イベントはネイティブイベントがトリガーされた後に収集されてトリガーされるため、ネイティブイベントが送信を防ぐために stopPropagation を呼び出すと、ルートノードにまったく到達できず、React にバインドされた処理関数をトリガーできず、当然合成イベントはトリガーされません。したがって、ネイティブイベントは合成イベントの送信を防ぐのではなく、React にバインドされたイベント関数の実行を防ぎます。
<div ネイティブ onClick={(e)=>{e.stopPropagation()}}>
  <div onClick={()=>{console.log("合成イベント")}}>合成イベント</div>
</div>

たとえば、この例では、ネイティブの onClick が送信をブロックした後、コンソールは「合成イベント」という 4 つの単語さえ入力しません。

以上がReactイベントメカニズムソースコード解析の詳細な内容です。Reactイベントメカニズムソースコードの詳細については、123WORDPRESS.COMの他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • React 合成イベントの説明
  • Reactでのイベントバインディングの実装は3つの方法を指しています
  • Reactイベントスロットリング効果が失敗する理由と解決策
  • Reactフォームとイベントの詳細な分析
  • React 学習: JSX と React イベント例の分析
  • 例を通してReactのイベントスロットリングとアンチシェイクを学ぶ
  • React におけるイベントバブリング防止の問題の詳細な分析
  • Python の Twisted フレームワークにおけるリアクター イベント マネージャーの使用に関する詳細な説明
  • Reactの合成イベントとネイティブイベントの理解

<<:  mysql5.7.19 winx64 インストールおよび構成方法のグラフィック チュートリアル (win10)

>>:  Centos7のホスト名を変更する3つの方法

推薦する

Linux CentOS 6.5 ifconfig が IP を照会できない問題の解決方法

最近、何人かの友人から、仮想マシンに CentOS をインストールした後、ifconfig コマンド...

MySQL msiバージョンのダウンロードとインストールの初心者向けの詳細なグラフィックチュートリアル

目次1. MySQL msiバージョンをダウンロードする2. インストール3. 環境変数を設定する1...

WebプロジェクトをIdeaにインポートし、Tomcatに公開する問題を解決します

Idea は既存の Web プロジェクトをインポートして Tomcat に公開しますが、Tomcat...

Vue モバイル開発で better-scroll を使用するときにクリック イベントが失敗する問題の解決策

最近、モバイル プロジェクトの開発方法を学ぶために vue を使用し、スクロールには better-...

マークアップ言語 - Web アプリケーション CSS スタイル

123WORDPRESS.COM HTML チュートリアル セクションに戻るには、ここをクリックして...

MySQL 集計関数のネストされた使用操作

目的: MySQL 集計関数のネストされた使用集計関数は直接ネストできません。例: max(coun...

XHTML チュートリアル: Transitional と Strict の違い

実際、XHTML 1.0 は、Transitional DOCTYPE と Strict DOCTY...

ディレクトリスクロール効果を実現するネイティブJS

これはネイティブ JS で実装されたテキスト スクロール効果です。この効果は通常、ニュース、ダイナミ...

vue3 タイムスタンプ変換 (フィルターを使用せずに)

vue2 では、タイムスタンプを変換するときに、通常はフィルターを使用します。vue3 以降では、...

Hadoopカウンターとデータクリーニングの適用

データクリーニング (ETL)コアビジネスの MapReduce プログラムを実行する前に、まずデー...

axios でリクエストをキャンセルし、重複リクエストを防ぐ方法について簡単に説明します。

目次序文コア - キャンセルトークン実用的なアプリケーションとパッケージングいくつかの小さな詳細序文...

Linux自動ログイン例の説明

インターネット上には、expect を使用して自動ログインを実現するスクリプトが多数存在しますが、明...

vscodeを使用してReact Native開発環境を構築する方法を教えます

質問コードにはプロンプトがありません: RN 開発に不慣れな、フロントエンド以外の学生の多くは、「ど...

VMware で Nginx+KeepAlived クラスタ デュアルアクティブ アーキテクチャを展開する際の問題と解決策

序文負荷分散には nginx を使用します。アーキテクチャのフロントエンドまたは中間層として、トラフ...

Windows 10 での MySQL 8.0.22 のインストールと設定方法のグラフィック チュートリアル

MySQL 8.0.22のインストールと設定方法のグラフィックチュートリアル、参考までに、具体的な内...