日本語版
最新ニュース
科学&テクノロジー

400 行のコードで独自の React.js を構築する

React v19 ベータ版 リリースされました。 React 18 と比較すると、中心となる原則はほとんど変わっていませんが、多くのユーザーフレンドリーな API が提供されています。 React をしばらく使ったことがあるかもしれませんが、内部でどのように機能するか知っていますか? この記事は、非同期更新をサポートし、中断できる約 400 行のコードを含むバージョンの React を構築するのに役立ちます。これは、多くの高レベル API が依存する React の中核機能です。 最終的なエフェクトの Gif は次のとおりです。 現在、私のサイトでホストされています GitHubにアクセスすることもできます。 オンライン版 自分で試してみること。 原則を説明する前に、 ミニ反応.ts、JSX が何を表すかを理解することが重要です。 JSX を使用して DOM を記述し、JavaScript ロジックを簡単に適用できます。 ただし、ブラウザは JSX をネイティブに理解できないため、記述された…

400 行のコードで独自の React.js を構築する

1716200103
2024-05-17 13:01:14

React v19 ベータ版 リリースされました。 React 18 と比較すると、中心となる原則はほとんど変わっていませんが、多くのユーザーフレンドリーな API が提供されています。 React をしばらく使ったことがあるかもしれませんが、内部でどのように機能するか知っていますか?

この記事は、非同期更新をサポートし、中断できる約 400 行のコードを含むバージョンの React を構築するのに役立ちます。これは、多くの高レベル API が依存する React の中核機能です。 最終的なエフェクトの Gif は次のとおりです。

現在、私のサイトでホストされています GitHubにアクセスすることもできます。 オンライン版 自分で試してみること。

原則を説明する前に、 ミニ反応.ts、JSX が何を表すかを理解することが重要です。 JSX を使用して DOM を記述し、JavaScript ロジックを簡単に適用できます。 ただし、ブラウザは JSX をネイティブに理解できないため、記述された JSX はブラウザが理解できる JavaScript にコンパイルされます。

ここでは babel を使用しましたが、もちろん他のビルド ツールを使用することもでき、生成されるコンテンツは同様になります。

したがって、それが呼び出していることがわかります React.createElement、次のオプションが提供されます。

  1. type: 現在のノードのタイプを示します。 div

  2. config: 現在の要素ノードの属性を表します。例: {id: "test"}

  3. Children: 子要素。複数の要素、単純なテキスト、または React.createElement によって作成された複数のノードが可能です。

あなたが経験豊富な React ユーザーであれば、React 18 より前は次のことが必要だったことを思い出しているかもしれません。 import React from 'react'; JSX を正しく書きます。 React 18 以降、これは必要なくなり、開発者のエクスペリエンスが向上しましたが、 React.createElement はまだ下で呼ばれています。

簡素化された React 実装では、次の設定を行う必要があります。 素早くreact({ jsxRuntime: 'classic' }) JSX を直接コンパイルして React.createElement 実装。

// Text elements require special handling.
const createTextElement = (text: string): VirtualElement => ({
  type: 'TEXT',
  props: {
    nodeValue: text,
  },
});

// Create custom JavaScript data structures.
const createElement = (
  type: VirtualElementType,
  props: Recordstring, unknown> = {},
  ...child: (unknown | VirtualElement)[]
): VirtualElement => {
  const children = child.map((c) =>
    isVirtualElement(c) ? c : createTextElement(String(c)),
  );

  return {
    type,
    props: {
      ...props,
      children,
    },
  };
};

次に、JSX を実際の DOM にレンダリングするために、前に作成したデータ構造に基づいてレンダリング関数の簡易バージョンを実装します。

// Update DOM properties.
// For simplicity, we remove all the previous properties and add next properties.
const updateDOM = (DOM, prevProps, nextProps) => {
  const defaultPropKeys = 'children';

  for (const [removePropKey, removePropValue] of Object.entries(prevProps)) {
    if (removePropKey.startsWith('on')) {
      DOM.removeEventListener(
        removePropKey.substr(2).toLowerCase(),
        removePropValue
      );
    } else if (removePropKey !== defaultPropKeys) {
      DOM[removePropKey] = '';
    }
  }

  for (const [addPropKey, addPropValue] of Object.entries(nextProps)) {
    if (addPropKey.startsWith('on')) {
      DOM.addEventListener(addPropKey.substr(2).toLowerCase(), addPropValue);
    } else if (addPropKey !== defaultPropKeys) {
      DOM[addPropKey] = addPropValue;
    }
  }
};

// Create DOM based on node type.
const createDOM = (fiberNode) => {
  const { type, props } = fiberNode;
  let DOM = null;

  if (type === 'TEXT') {
    DOM = document.createTextNode('');
  } else if (typeof type === 'string') {
    DOM = document.createElement(type);
  }

  // Update properties based on props after creation.
  if (DOM !== null) {
    updateDOM(DOM, {}, props);
  }

  return DOM;
};

const render = (element, container) => {
  const DOM = createDOM(element);
  if (Array.isArray(element.props.children)) {
    for (const child of element.props.children) {
      render(child, DOM);
    }
  }

  container.appendChild(DOM);
};

ファイバー アーキテクチャと同時実行モードは、主に、完全な要素ツリーが一度再帰されると再帰できないという問題を解決するために開発されました。 中断された、メインスレッドが長期間ブロックされる可能性があります。 ユーザー入力やアニメーションなどの優先度の高いタスクは、適時に処理されない可能性があります。

ソース コードでは、作業は小さな単位に分割されます。 ブラウザーがアイドル状態になると、ブラウザーはこれらの小さな作業単位を処理し、メイン スレッドの制御を放棄して、ブラウザーが優先度の高いタスクに迅速に応答できるようにします。 ジョブの小さな単位がすべて完了すると、結果が実際の DOM にマッピングされます。

実際の React では、提供されている API を次のように使用できます。 useTransition または useDeferredValue 更新の優先順位を明示的に下げることができます。

つまり、ここでの 2 つの重要なポイントは、メインスレッドを放棄する方法と、作業を管理可能な単位に分割する方法です。

requestIdleCallback

requestIdleCallback は、ブラウザがアイドル状態のときにコールバックを実行する実験的な API です。 まだサポートされていません すべてのブラウザ。 React では、 スケジューラパッケージこれには、タスクの優先順位の更新など、requestIdleCallback よりも複雑なスケジューリング ロジックがあります。

ただし、ここでは非同期割り込み可能性のみを考慮するため、これは React を模倣する基本的な実装です。

// Enhanced requestIdleCallback.
((global: Window) => {
  const id = 1;
  const fps = 1e3 / 60;
  let frameDeadline: number;
  let pendingCallback: IdleRequestCallback;
  const channel = new MessageChannel();
  const timeRemaining = () => frameDeadline - window.performance.now();

  const deadline = {
    didTimeout: false,
    timeRemaining,
  };

  channel.port2.onmessage = () => {
    if (typeof pendingCallback === 'function') {
      pendingCallback(deadline);
    }
  };

  global.requestIdleCallback = (callback: IdleRequestCallback) => {
    global.requestAnimationFrame((frameTime) => {
      frameDeadline = frameTime + fps;
      pendingCallback = callback;
      channel.port1.postMessage(null);
    });
    return id;
  };
})(window);

いくつかの重要なポイントを簡単に説明します。

主に、マクロタスクを使用して単位タスクの各ラウンドを処理します。 しかし、なぜマクロタスクなのでしょうか?

これは、マクロタスクを使用してメインスレッドの制御を放棄し、このアイドル期間中にブラウザが DOM を更新したりイベントを受信したりできるようにする必要があるためです。 ブラウザーは別のタスクとして DOM を更新するため、現時点では JavaScript は実行されません。

メインスレッドは一度に 1 つのタスクのみを実行できます。つまり、JavaScript の実行、または DOM 計算、スタイル計算、入力イベントなどの処理のいずれかです。 Promise.then) ただし、メインスレッドの制御は手放しません。

これは、最新のブラウザでは、ネストされた setTimeout 呼び出しが 5 回を超えてブロックされていると見なされ、最小遅延が 4 ミリ秒に設定されているため、精度が十分ではありません。

アルゴリズム

React は進化し続けており、ここで説明するアルゴリズムは最新のものではない可能性がありますが、その基本を理解するには十分であることに注意してください。

以下は、ワークユニット間の接続を示す図です。

React では、各作業単位をファイバー ノードと呼びます。 これらは、リンクされたリストのような構造を使用して相互にリンクされています。

  1. 子供: 親ノードから最初の子要素へのポインタ。

  2. 戻り/親: すべての子要素には親要素へのポインタがあります。

  3. 兄弟: 最初の子要素から次の兄弟要素までを指します。

私たちは単に拡張しているだけです 与える ロジック、呼び出しシーケンスを再構築 workLoop -> performUnitOfWork -> reconcileChildren -> commitRoot

  1. ワークループ : 電話して空き時間を取得 requestIdleCallback 継続的に。 現在アイドル状態で、実行する単位タスクがある場合は、各単位タスクを実行します。

  2. 作業単位を実行する: 実行された特定の単位タスク。 これはリンクリストの考え方を具体化したものです。 具体的には、一度に 1 つのファイバー ノードのみが処理され、次に処理されるノードが返されます。

  3. 和解する子供たち: 現在のファイバー ノードを調整します。これは実際には仮想 DOM の比較であり、加えられる変更を記録します。 各ファイバー ノードで直接変更して保存したことがわかります。これは、JavaScript オブジェクトに対する単なる変更であり、実際の DOM には触れていないためです。

  4. コミットルート: 現在アップデートが必要な場合 (によると) wipRoot)そして、処理する次の単位タスクはありません(によると) !nextUnitOfWork)、これは、仮想変更を実際の DOM にマッピングする必要があることを意味します。 の commitRoot ファイバーノードの変更に応じて実際の DOM を変更することです。

これらを使用すると、ファイバー アーキテクチャを実際に使用して割り込み可能な DOM 更新を行うことができますが、まだトリガーが不足しています。

React では、一般的なトリガーは次のとおりです。 useState、最も基本的な更新メカニズム。 これを実装してファイバー エンジンを起動しましょう。

具体的な実装は次のとおりです。

// Associate the hook with the fiber node.
function useStateS>(initState: S): [S, (value: S) => void] {
  const fiberNode: FiberNodeS> = wipFiber;
  const hook: {
    state: S;
    queue: S[];
  } = fiberNode?.alternate?.hooks
    ? fiberNode.alternate.hooks[hookIndex]
    : {
        state: initState,
        queue: [],
      };

  while (hook.queue.length) {
    let newState = hook.queue.shift();
    if (isPlainObject(hook.state) && isPlainObject(newState)) {
      newState = { ...hook.state, ...newState };
    }
    if (isDef(newState)) {
      hook.state = newState;
    }
  }

  if (typeof fiberNode.hooks === 'undefined') {
    fiberNode.hooks = [];
  }

  fiberNode.hooks.push(hook);
  hookIndex += 1;

  const setState = (value: S) => {
    hook.queue.push(value);
    if (currentRoot) {
      wipRoot = {
        type: currentRoot.type,
        dom: currentRoot.dom,
        props: currentRoot.props,
        alternate: currentRoot,
      };
      nextUnitOfWork = wipRoot;
      deletions = [];
      currentRoot = null;
    }
  };

  return [hook.state, setState];
}

フックの状態をファイバー ノード上に巧みに保持し、キューを通じて状態を変更します。 ここから、React フック呼び出しの順序を変更してはいけない理由もわかります。

私たちは、非同期で中断可能な更新をサポートする React の最小限のモデルを実装しました。依存関係はなく、コメントと型を除くと、コードは 400 行未満になる可能性があります。 お役に立てれば幸いです。

私のコンテンツが役に立ったと思われる場合は、お願いします 購読を検討してください。 を送ります 週刊ニュースレター毎週日曜日 最新の Web 開発アップデートを含みます。 ご協力ありがとうございました!

#行のコードで独自の #React.js #を構築する

執筆者について: nipponese

Nipponese News編集部は、国内外のニュースを日本語で分かりやすくお届けします。