こんにちは 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 からご参加ください!
- Kyoto.js Slack 登録ページ: http://kyotojs.herokuapp.com/
- これまでのhangoutのまとめ: https://github.com/kyotojs/meetup/issues/4
準備
ここでは、 facebook/react のレポジトリの構造について簡単に説明します。
React の処理の概要やディレクトリ構成については、公式のドキュメントである程度解説されています。
英語が苦でない方は読んでおくと良いでしょう。
- Codebase Overview: https://facebook.github.io/react/contributing/codebase-overview.html
- Implementation Notes: https://facebook.github.io/react/contributing/implementation-notes.html
- 僕が↑を読んだメモ: https://gist.github.com/fand/34be220de534d795aa849a69198385fd
まずはレポジトリを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パッケージで重複するコードを管理しやすくなったり、デバッグが容易になるといったメリットがあるようです。
- monorepo について: https://github.com/babel/babel/blob/master/doc/design/monorepo.md
- lerna: https://github.com/lerna/lerna
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.js
と Gruntfile.js
が両方存在するのに気づきます。
Issueにもある通り、 Grunt から Gulp へ移行途中ということみたいですね。
2013年時点のコード を見てみると、まだ Grunt だけ使っていることがわかります。
Gruntfile の初っ端から gulp を呼ぶための関数を定義しており、涙ぐましい……。
タスクごとに移行していくというのはスマートですね。
大規模なプロジェクトで Grunt から Gulp への移行を検討している方は真似してみても良いかもしれません。
コードリーディング
それでは、早速コードを読んで見ましょう!
今回は React アプリケーションの初期化まわりを探ることにします。
具体的には、 React.render()
により React コンポーネントが初期化され、DOM 要素へマウントされるまでの処理を追うことを目標とします。
src/
以下のファイルは、主に addons
isomorphic
そして renders
の3つに分類されています。
React のコア部分が isomorphic
に、 react-dom
や react-native
等環境に依存したコードが renderes/
に入っています。
react
まずは react
パッケージのコア部分から攻めてみましょう。
react
パッケージの本体は src/isomorphic/React.js
です。
require('react');
した時に export される API が定義されています。
APIは Modern
Classinc
に分類されています。
Modern の方には Component や PureComponent といった ES2015 Class 前提のモジュールがあり、 Classic には createClass や mixin といった懐かしい単語が並んでいますね。
特に重要なのは ReactComponent
と ReactElement
でしょうか。
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 を渡しているのが見つかりました。
mountComponent
はHTML文字列を作って返すメソッドです。
後述しますが、ReactCompositeComponent は ReactComponent に対応する内部表現オブジェクトのクラスであり、ReactComponent の子クラスとかでは ありません 。
mountComponent でコンポーネント毎の HTML 文字列を生成し、どこかで組み立てているようですが……?
react-dom
今度は react-dom
のコードを見てみましょう。
react-dom
の本体は src/renderers/dom/ReactDOM.js
にあります。
React.js 同様、 require
してAPIを公開してるだけですね。
(unmountComponentAtNode
なんて出来たんですね……知らなかった)
ReactDOM.render
は ReactMount
で定義されているようです。
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 と呼んでいます。
さらに分類すると以下のようになります (ドキュメントを参照)。
- public instance: ユーザーが定義する React コンポーネントのインスタンス (ReactComponent)
- internal instance: public 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