mongolyyのブログ

開発(Javascript, Typescript, React, Next.js)や開発手法(スクラム, アジャイル)、勉強したことについて色々書ければと。

「Building React From Scratch」動画の視聴メモ(前編)

はじめに

最近「Reactハンズオンラーニング」という書籍を読んでいるのですが、

Reactの基本として、jsxを使用しないプリミティブな書き方をしていて、Reactが担っている役割に興味が出てきたので、以下の「Building React From Scratch」という動画を読みて理解を深めることにしました。
全編英語だったので、整理のためにメモを残したいと思います。


www.youtube.com

Githubリポジトリ

github.com

動画視聴メモ

クラッチで作るライブラリの名前はDilithium(https://youtu.be/_MAD4Oly9yg?t=212)。スタートレック用語ぽい (ダイリチウム - Wikipedia)。

Top LevelのAPIは以下の3つ(https://youtu.be/_MAD4Oly9yg?t=231

  • createElement
  • Component
  • render

ComponentクラスのAPIは以下の5つ(https://youtu.be/_MAD4Oly9yg?t=256

  • constructor()
  • render()
  • setState()
  • this.props
  • this.state

実際にうまく動いているかテストするコンポーネントとして、値が変わり、それと連動して色が変化するボタン CounterButton なるものを作った(https://youtu.be/_MAD4Oly9yg?t=320

以上で準備は完了。
実装をやっていく。

まずはcreateElement。(https://youtu.be/_MAD4Oly9yg?t=390
Github上では https://github.com/zpao/building-react-from-scratch/blob/a61c831266c7521869ba0293b06c3e2c238a0230/dilithium/src/Element.js

function createElement(type, config, children) {
  // Clone the passed in config (props). In React we move some special
  // props off of this object (keys, refs).
  let props = Object.assign({}, config);

  // Build props.children. We'll make it an array if we have more than 1.
  let childCount = arguments.length - 2;
  if (childCount === 1) {
    props.children = children;
  } else if (childCount > 1) {
    props.children = Array.prototype.slice.call(arguments, 2);
  }

  // React Features not supported:
  // - keys
  // - refs
  // - defaultProps (usually set here)

  return {
    type,
    props,
  };
}

まずは、configを取得してpropsとして、ディープコピー
'Array.prototype.slice.call(arguments, 2);' は初めて見たが、第3引数以降の引数を配列にする書き方と理解
propsのchildrenに子コンポーネントをセットして、returnしている

次は、render(https://youtu.be/_MAD4Oly9yg?t=449Githubでは https://github.com/zpao/building-react-from-scratch/blob/a61c831266c7521869ba0293b06c3e2c238a0230/dilithium/src/Mount.js

function render(element, node) {
  assert(Element.isValidElement(element));

  // First check if we've already rendered into this node.
  // If so, we'll be doing an update.
  // Otherwise we'll assume this is an initial render.
  if (isRoot(node)) {
    update(element, node);
  } else {
    mount(element, node);
  }
}

説明でも言っているが、めっちゃシンプル。
すでにレンダーされていたら更新するし、レンダーされていなければ、マウント(初期化、インスタンス化、レンダー)する。

次はマウント(https://youtu.be/_MAD4Oly9yg?t=509)。

function mount(element, node) {
  // Mark this node as a root.
  node.dataset[ROOT_KEY] = rootID;

  // Create the internal instance. We're assuming for now that we only have
  // `Component`s being rendered at the root.
  let component = instantiateComponent(element);

  instancesByRootID[rootID] = component;

  // This will return a DOM node. React does more work here to determine if we're remounting
  // server-rendered content.
  let renderedNode = Reconciler.mountComponent(component, node);

  // Empty out `node` so we can put it under our control.
  DOM.empty(node);

  DOM.appendChild(node, renderedNode);

  // Incrememnt rootID so we can track appropriately.
  rootID++;
}

コンポーネントの初期化をして、そのコンポーネントを、rootIDをキーとして、instancesByRootIDというオブジェクトに格納している。
instancesByRootIDはHashMapのイメージで使われている感じかな?
その後、ReconcilerのmountComponentでDOMノードを取得しているよう。

説明ではReconcilerはReactの中で複雑な部分を抽象化してくれていると言っている。
詳細な説明はこの後あるらしい。
あと、Reconcilerのmountは再帰的であることが重要らしい。このコードだけ見てもよくわからん。

次は、update(https://youtu.be/_MAD4Oly9yg?t=597)。

function update(element, node) {
  // Ensure we have a valid root node
  assert(node && isRoot(node));

  // Find the internal instance and update it
  let id = node.dataset[ROOT_KEY];

  let instance = instancesByRootID[id];

  if (shouldUpdateComponent(instance, element)) {
    Reconciler.receiveComponent(
      instance,
      element
    );
  }
  } else {
    // Unmount and then mount the new one
    unmountComponentAtNode(node);
    mount(element, node);
  }

  // TODO: update
}

githubではTODOとなっているところがあったが、動画中ではそうなっていなかったので適宜修正した)

updateは shouldUpdateComponent 関数で、updateであったり、更新ができないコンポーネントであれば再マウントしたりしている。

#receiveComponent はcodemodのようなものをしているとのこと。
codemodは初耳だったが facebookが過去に作ったレファクタリングのツールっぽい github.com

本来は全てのコンポーネントに対して、updateがかかっていくが、これは高負荷なので、ショートカットの仕組みがあるとのこと

ショートカットの仕組みの一つとして shouldUpdateComponent が存在する(https://youtu.be/_MAD4Oly9yg?t=677

function shouldUpdateComponent(prevElement, nextElement) {
  let prevType = typeof prevElement;
  let nextType = typeof nextElement;

  // Quickly allow strings.
  if (prevType === 'string') {
    return nextType === 'string';
  }

  // Otherwise look at element.type. In React we would also look at the key.
  return prevElement.type === nextElement.type;
}

これはelementのtypeをチェックして、同じであればtrue、異なっていればfalseを返す。
elementのタイプが異なるとき、そのコンポーネントのサブツリーは大きく変更されることが多く、その場合に書き換えるのではなく、破棄して再マウントするほうがコストは低いということのよう。

おわり

とりあえず長くなりそうなので、分割しようと思います。

とりあえず、renderですでに描画されているかチェックして、描画されているなら、updateをかけ、描画されていないなら、mountする。

レンダー周りで複雑なことはReconcilerがやっており、 DOMノードを生成したり、更新したりする処理はReconcilerでやっているとのこともわかりました。

また、更新処理において、パフォーマンスが悪くならないような工夫もされているとのこともわかりました。

次回は Reconciler からです。(https://youtu.be/_MAD4Oly9yg?t=759