読者です 読者をやめる 読者になる 読者になる
月 の 上

React のソースコードを読んでみよう!

f:id:amagitakayosi:20161207035726p:plain

こんにちは id:amagitakayosi です。
株式会社はてなで主にフロントエンド開発を担当しています。

この記事では React 本体のコードを読んでみます!!

この記事は Reactアドベントカレンダー 2016 の7日目の記事です。
昨日は yutaszk さんで「react-router v4 でFlux アプリケーションをHot Module Replacement する」でした。

目次

はじめに (Kyoto.js の宣伝)

僕は Kyoto.js というコミュニティを運営しています。
Kyoto.js は京都界隈の JavaScript 開発者のためのコミュニティです。
(実際は京都以外のメンバーのほうが多い……)

これまで Kyoto.js では3,4ヶ月おきに勉強会を開催してきましたが、もっと気軽に交流したい!活動を増やしたい!ということで、先月からオンラインコードリーディング会をすることにしました。

コードリーディングは1,2週間おきに1度のペースで、Google hangoutを使って開催しています。
興味のある方は是非 Slack からご参加ください!

準備

github.com

ここでは、 facebook/react のレポジトリの構造について簡単に説明します。

React の処理の概要やディレクトリ構成については、公式のドキュメントである程度解説されています。
英語が苦でない方は読んでおくと良いでしょう。

まずはレポジトリをcloneしてきましょう。
cloneしたら、npm installなどの準備もしておきます。

# まずはクローン
git clone https://github.com/facebook/react

# yarnをつかって依存パッケージをインストール
npm install -g yarn
yarn

# react.js 等をビルド。エラー出るかもしれないけど大丈夫!
npm run build

いくつか注意点!

  • 本記事では 2be0d93c 時点でのコードを参照します。
  • 行番号は (L123) のように表します。

ディレクトリ構造

まずは、レポジトリがどんな構造になってるのか見てみましょう。
ディレクトリ構造は以下のようになっています(面白いとこだけ抜粋)。

react/
├─ src/
│   ├─ addons/
│   ├─ isomorphic/
│   │   ├── React.js
│   │   ├── children/
│   │   ├── classic/
│   │   ├── hooks/
│   │   └── modern/
│   ├─ renderers/
│   │   ├── art/
│   │   ├── dom/
│   │   ├── native/
│   │   ├── noop/
│   │   ├── shared/
│   │   └── testing/
│   └─ testing/
├─ build/
├─ grunt/
├─ gulp/
├─ packages/
├─ Gruntfile.js
├─ gulpfile.js
├─ package.json
└── yarn.lock

monorepo

package.jsonを見ると、このレポジトリ自体のパッケージ名は react-build となっています。
また、 packages/ を見ると、複数のnpmパッケージのpackage.jsonが置かれています。

このように、 React では一つのgitレポジトリで複数のnpmパッケージを管理する構成をとっています。
このような構成のレポジトリは monorepo と呼ばれています。 monorepo は他にも Babel や Angular などの大規模なプロジェクトで採用されています。
また、 monorepo を管理するための lerna というツールも存在します。
monorepo を採用することで、 複数のnpmパッケージで重複するコードを管理しやすくなったり、デバッグが容易になるといったメリットがあるようです。

Haste

src/ 以下のファイルを眺めると、 require('ReactMount') のように、ファイル名べた書きの不思議な require に出くわします。
これは Facebook 内製の Haste というモジュールシステムによるものです。

Haste では、 全てのファイル名をユニークにする という制約を設けています。
ビルド時には、browserify の前段で全てのファイルを lib/ 直下にコピーすることで、 browserify が require() を解決出来るようになっています。

React の src/ はかなり複雑な構造になっていますが、ファイル名がユニークであることで、モジュール名さえわかっていればエディタのfuzzy finderを使って簡単にファイルを開ける、といったメリットがあるようです。

Haste のコードが Facebook 内部でどうやって管理されているのかは分かりませんが、 React においては、この辺で require() しているスクリプトで同様の機能を実現しているようです。
https://github.com/facebook/react/blob/2be0d93c7782eb2dad62efcac9668152da715c25/gulpfile.js#L18-L20

Gulp と Grunt

gulpfile.jsGruntfile.js が両方存在するのに気づきます。
Issueにもある通り、 Grunt から Gulp へ移行途中ということみたいですね。
2013年時点のコード を見てみると、まだ Grunt だけ使っていることがわかります。

Gruntfile の初っ端から gulp を呼ぶための関数を定義しており、涙ぐましい……。
タスクごとに移行していくというのはスマートですね。
大規模なプロジェクトで Grunt から Gulp への移行を検討している方は真似してみても良いかもしれません。

コードリーディング

それでは、早速コードを読んで見ましょう!
今回は React アプリケーションの初期化まわりを探ることにします。
具体的には、 React.render() により React コンポーネントが初期化され、DOM 要素へマウントされるまでの処理を追うことを目標とします。

src/ 以下のファイルは、主に addons isomorphic そして renders の3つに分類されています。
React のコア部分が isomorphic に、 react-domreact-native 等環境に依存したコードが renderes/ に入っています。

react

まずは react パッケージのコア部分から攻めてみましょう。

react パッケージの本体は src/isomorphic/React.js です。
require('react'); した時に export される API が定義されています。

https://github.com/facebook/react/blob/2be0d93c7782eb2dad62efcac9668152da715c25/src/isomorphic/React.js#L54-L92

APIModern Classinc に分類されています。
Modern の方には Component や PureComponent といった ES2015 Class 前提のモジュールがあり、 Classic には createClass や mixin といった懐かしい単語が並んでいますね。

特に重要なのは ReactComponentReactElement でしょうか。

ReactComponent

ソース: src/isomorphic/modern/class/ReactComponent.js

ReactComponent は、我々通常の React ユーザーがコンポーネント作成時に extend するクラスです。
このファイルでは setState forceUpdate が定義されています。
いずれのメソッドでも、 this.state を直接操作したりせず、 this.updater に処理を enqueue するにとどまっています。

下の方で if (__DEV__) としているのは、開発環境で警告を出すためのコードです。
React は非常に丁寧にの警告やエラーを出してくれますが、コードを読んでいると至る所に warning invariant といった警告、エラーの為の処理が挟まれています。
これらの関数は facebook/fbjs 内で定義されています。
fbjs は Facebook 内製のツール群をまとめたレポジトリです。
https://github.com/facebook/fbjs/blob/e66ba20ad5be433eb54423f2b097d829324d9de6/packages/fbjs/src/forks/warning.js

話が逸れました。
さて、 updater はデフォルトでは ReactNoopUpdateQueue が渡されるようですが、これは名前の通り何もしない updater です。
実際に render するときは、適切な updater が渡されるのでしょうか?

git grep updater src でそれらしい箇所を探すと、 ReactCompositeComponent の mountComponent メソッドで updateQueue を渡しているのが見つかりました。

https://github.com/facebook/react/blob/2be0d93c7782eb2dad62efcac9668152da715c25/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L197

mountComponent はHTML文字列を作って返すメソッドです。
後述しますが、ReactCompositeComponent は ReactComponent に対応する内部表現オブジェクトのクラスであり、ReactComponent の子クラスとかでは ありません
mountComponent でコンポーネント毎の HTML 文字列を生成し、どこかで組み立てているようですが……?

react-dom

今度は react-dom のコードを見てみましょう。
react-dom の本体は src/renderers/dom/ReactDOM.js にあります。
React.js 同様、 require してAPIを公開してるだけですね。

https://github.com/facebook/react/blob/2be0d93c7782eb2dad62efcac9668152da715c25/src/renderers/dom/ReactDOM.js#L31-L41

(unmountComponentAtNode なんて出来たんですね……知らなかった)

ReactDOM.renderReactMount で定義されているようです。

ReactMount

ソース: src/renderers/dom/stack/client/ReactMount.js

ReactMount は Mounting に関する処理を行うモジュールです。
Mounting とは、 React コンポーネントを render し、 container 要素内に DOM ツリーを挿入する処理のことです。

// ReactMount.jsより抜粋
ReactMount.render(
  component,
  document.getElementById('container')
);
<div id="container">       <-- Supplied `container`.
  <div data-reactid=".3">  <-- Rendered reactRoot of React component.
    // ...                              
  </div>
</div>

render メソッドの定義は以下のようになっています。

  /**
   * @param {ReactElement} nextElement Component element to render.
   * @param {DOMElement} container DOM element to render into.
   * @param {?function} callback function triggered on completion
   * @return {ReactComponent} Component instance rendered in `container`.
   */
  render: function(nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
  },

ReactElement は、type props といったプロパティをもつ Plain なオブジェクトです。
多くの場合は JSX から生成し、 render() で return したりします。
JSX が苦手な方は手動で React.createElement() してるかもしれませんね。
(参考: https://facebook.github.io/react/docs/introducing-jsx.html)

_renderSubtreeIntoContainer は次のような処理をしています。

  • nextElement のラッパーを作成 (L464)
  • _renderNewRootComponent() で取得した ReactComponent のインスタンスを返す (L528)

_renderNewRootComponent の中身はこんな感じ:

  • instantiateReactComponent()nextElementインスタンス化 (L382)
  • ReactUpdates.batchedUpdates() を呼んで、取得した ReactComponent インスタンスを DOM ツリーにマウント (L394)。

「ReactUpdates って何やねん」という感じですが、今は気にしなくて良いです。

batchedUpdates() の引数を辿っていくと、mountComponentIntoNode に行き着きます (L94)。
この関数では、 ReactReconciler.mountComponent()markup を作って _mountImageIntoNode() に渡しています。
_mountImageIntoNode では setInnerHTML したりしてるので (L726)、 markup は HTML 文字列とみていいでしょう。

今度は ReactReconciler を読んで、 markup がどのように作られているか探って行きましょう。

ReactReconciler

ソース: src/renderers/shared/stack/reconciler/ReactReconciler.js

ReactReconciler は一言で言うと「ReactElement、ReactComponent、DOM要素、内部表現をまとめる者」です。

reconcile /rék(ə)nsàɪl/
調和させる, 調整する, 一致させる «with» ; 〈銀行明細など〉と帳尻を合わせる

先ほど「ReactCompositeComponent は ReactComponent に対応する内部表現」と説明しました。
React 内部では、 ReactComponent など render() を持つオブジェクトを public instance , ReactCompositeComponent などの内部表現を internal instance と呼んでいます。

さらに分類すると以下のようになります (ドキュメントを参照)。

internal instance は、対応する public instance や ReactElement への参照を持ちます。
また、 componentDidMount など public instance の持つライフサイクルを呼び出す役目を担っています。

以上を踏まえて ReactReconciler.mountComponent を眺めてみます。
第1引数は internalInstance という名前になっていて分かりやすいですね。 *1
internalInstance.mountComponent() で HTML 文字列を生成し、 refs などの処理をしてから返しています。

となると、今度は internal instance のクラスを読めばいいことがわかります。
composite component, host component の順番に読んでいきましょう。

ReactCompositeComponent

ソース: src/renderers/shared/stack/reconciler/ReactCompositeComponent.js

ファイルを開くといきなり良い図がでてきます。
この図では、public instance のライフサイクルメソッドや render が実行される順序が示されています。
以下転載 (ソース)。

/**
 * ------------------ The Life-Cycle of a Composite Component ------------------
 *
 * - constructor: Initialization of state. The instance is now retained.
 *   - componentWillMount
 *   - render
 *   - [children's constructors]
 *     - [children's componentWillMount and render]
 *     - [children's componentDidMount]
 *     - componentDidMount
 *
 *       Update Phases:
 *       - componentWillReceiveProps (only called if parent updated)
 *       - shouldComponentUpdate
 *         - componentWillUpdate
 *           - render
 *           - [children's constructors or receive props phases]
 *         - componentDidUpdate
 *
 *     - componentWillUnmount
 *     - [children's componentWillUnmount]
 *   - [children destroyed]
 * - (destroyed): The instance is now blank, released by React and ready for GC.
 *
 * -----------------------------------------------------------------------------
 */

public instance の render が再帰的に呼ばれることがわかりますね。

mountComponent の処理の流れは次のようになっています。

  • public instance を生成 (L201-L262)
  • componentWillMount を呼ぶ (L336-L351)
  • markup を生成 (L353-L370)
  • componentDidMount を呼ぶ (L372-L384)

markup の生成は performInitialMount で行なわれます。 こちらは次のような流れ。

  • renderedElement を作る (L516)
  • renderedElement から子供の internal instance を生成 (L521)、
  • ReactReconciler.mountComponent を呼ぶ

renderedElement には public instance の render() が返す ReactElement が入ります (L516, L1184, L1153 の順で呼ばれる)。
この ReactElement は this._currentElement の子要素にあたります。

すなわち、 ReactReconciler.mountComponent()render()再帰的に呼び続ける構造になっています。
React のツリーの末端は必ず DOM 要素などの host component となるので、そこで処理が止まるようです。

ReactDOMComponent

ソース: src/renderers/dom/stack/client/ReactDOMComponent.js

ブラウザ環境での host component 実装は ReactDOMComponent にあります。
ReactHostComponent というのもありますが、これらは継承関係ではありません。 *2

mountComponent を読んでいくと……ありました!
ReactElement の type から document.createElement() したり、 HTML 文字列を生成したりしています! (L569, L604)
前者の場合、 _createInitialChildren を呼ぶことで DOM 要素に子要素を挿入しています。

これでようやく ReactDOM.render() 時の DOM 生成が完了しました!

まとめ

初期化処理をなぞっただけでも、沢山の登場人物がでてきました。
今回でてきた主な登場人物は以下のとおり。

  • ReactElement
  • public instance (ReactComponent)
  • internal instance
    • composite component (ReactCompositeComponent)
    • host component (ReactDOMComponent)
  • reconciler (ReactReconciler)

他にも updater や transaction が出てきましたが今回は割愛します。

ReactDOM.render() した時の処理の流れは、ざっくりいうと以下のとおりです。

  • ReactMount.render() を呼び
  • ReactMount._renderSubtreeIntoContainer() で一番上の ReactComponent をインスタンス化し
  • ReactReconciler.mountComponent()render()再帰的に呼び
  • ReactDOMComponent.prototype.mountComponent で DOM 要素を生成する

おわりに

後半箇条書きばっかりになってしまった 😇
React ユーザーの方もそうでない方も、この記事で React の実装に興味を持っていただけたら幸いです。

よろしければ Kyoto.js Slack も覗いてみてください!

それでは!!


この記事は Reactアドベントカレンダー 2016 の7日目の記事です。
明日は chimame さんで「Webpack依存のReactコンポーネントをテストする」です。

*1:コメントでは ReactComponent となっていますが、多分間違いだと思います

*2:ドキュメントで解説されています https://facebook.github.io/react/contributing/codebase-overview.html#dynamic-injection