FrontEnd

React Konvaのソースコードを読む

投稿日:

はじめに

前回の記事では、React KonvaにはReact Componentをchildrenに含めることで、Canvas上に描画できることが分かりました。どのようにCanvasへのレンダリングを行なっているか調べてみたいと思います。
たまには、オープンソースのコードを読むのも良いかなと思ったので、今回は読んで理解した内容を記事にまとめました。

React Konvaの大まかなアーキテクチャ

まず、ReactとReact DOMの責務を整理すると次のようになります。

  • React
    状態管理、仮想DOMの管理・変更検知
  • React DOM
    仮想DOMの変更検知に従い、実際にDOMをレンダリングする

React Konvaは React DOMに近い位置付けで、大まかに次のような仕組みとなっています。

  • <Stage>のchildrenに渡された描画用のコンポーネント(Circleなど)は、ReactKonvaのカスタムレンダラーによって、Konva.jsへの描画命令に変換されている。
  • React Konvaのカスタムレンダラーは、 react-reconciler を使って実装されている。(React DOMやReact Nativeも使っています)
  • つまり、<Stage>内の仮想DOMの管理や変更検知はReact Fiberに任せ、React Konvaは変更検知に従ってKonva.jsへの描画命令に変換することを責務の範囲としている。

ちなみに、react-reconcilerを使ったカスタムレンダラーは色々とあり、こちらのリポジトリにまとめられています。

コンポーネントがCanvas描画に変換される仕組み

Stageコンポーネント

Stageコンポーネントは、 StageWrap コンポーネントを生成するのみとなっています。

FiberProviderとは、its-fineリポジトリが提供しているものでStageWrapで useContextBridge() を使用するために、必要なプロバイダです。 useContextBridge() については、後ほど説明します。

StageWrap

StageWrapでは、Konva.Stageを生成し、useLayoutEffectによって、childrenに渡されたコンポーネント(Circleなど)の変化に応じてKonvaRendererへの伝達を行なっています。

このコンポーネントでは、2つの useLayoutEffect がメインの処理となっています。
まず、1つ目のuseLayoutEffectでは、第二引数が空配列となっているため、このコンポーネントの1回目のレンダリング時のみ呼び出され、Stageの初期化を行なっています。
以下に実装のポイントをまとめました。

  • childrenで渡されてきた<Circle />などのコンポーネントを、Konva.jsによってCanvasに描画させるため、独自のレンダラー KonvaRenderer を使用しています。
  • KonvaRendererの直下に、 Bridge コンポーネントを置います。
    • Bridgeは useContextBridge() によって生成される。このフックは、 its-fine という別の依存パッケージによるもので、これを使用すると異なるレンダラー間でContextを共有することができます。そのため、以下のように、通常のReactコンポーネントのように、Stage内でContextが共有されます。

2つ目の useLayoutEffect は、第二引数が省略されているため、StageWrap自体が再描画されようとするときに毎回呼ばれます。
ここでは、 applyNodeProps() でStage自身のpropsの変更を反映した後、 KonvaRenderer.updateContainer() によって、Canvasの内容を更新しています。( applyNodeProps() については後ほど説明します。)

KonvaRenderer

ここがReact Konvaの本題と言える部分になります。Fiberによって差分検知された仮想DOMを、実際にKonvaの描画処理に変換し、レンダリングする機能です。
KonvaRendererはReactKonvaCore.tsxに次のように定義されています。

ReactFiberReconciler という、Reactのレンダラーを作成するための機能を使用しています。引数のHostConfigにレンダリングに必要な実装をすることで、仮想DOMを実際にレンダリングします。
ちなみに、React Konvaに限らず、React DOMやReact Nativeも、このHostConfigを実装することで、実際のDOMやNativeのViewなどをレンダリングしています。詳しくはこちらのドキュメンントに説明があります
ここからは、描画用のコンポーネントがStageに追加された場合に、どのようにCanvasに描画されるか追っていきたいと思います。

createInstance

createInstance()関数は、仮想DOMが追加されたときに呼び出されます。

createInstanceで、仮想DOMとして渡した描画コンポーネントをもとに、Konvaのクラスからインスタンス生成し、returnします。ここでreturnしたインスタンスは、次に紹介するinsertBefore()に渡されます。
ちなみに、ここで生成した描画用のインスタンスは、全て Node クラスを継承しているので、こちらのドキュメントを見ながらコードを読み進めると、理解が深まりました。

applyNodeProps()

applyNodePropsは、propsとして渡された値を、実際にKonva Classのアトリビュートとしてセットしたり、イベントリスナーとして登録したりする関数です。

まず、渡されたoldPropsの内容を、Konva Classから一旦全部削除します。
on から始まるpropsはイベントリスナーとして扱われ、 instance.off(eventName, oldProps[key]) で削除され、その他のpropsは instance.setAttr(key, undefined) で削除します。

次に、渡されたpropsの内容をKonva Nodeに適用します。
こちらもイベントリスナーと他のpropsの分け方は削除時と似ています。イベントリスナーは一番最後のforループで instance.on(eventName + EVENTS_NAMESPACE, newEvents[eventName]) で登録し、その他のpropsは instance.setAttrs(updatedProps); でセットします。

ちなみに、上記のコードでは省略しましたが、実際のコードでは EVENTS_NAMESPACE は以下のように定義されています。

Konva Nodeのイベンスト名には名前空間を指定することができ、実際はReact Konva経由で登録したイベントリスナーはすべてこの名前空間が充てられます。詳細はこちらのドキュメントで説明されています。
おそらく、jQueryの on() と同じような概念で、リスナーを名前空間ごとに管理できるようになる仕組みです。(こちらのjQueryの説明記事が分かりやすいです。)
React Konvaでの名前空間の使いどころとしては、後述するKonva Rendererでの実装で、仮想DOMが削除され、実際のKonva Nodeも削除する必要があるときに、全てのイベントを削除するために使われています。

insertBefore()

parentInstanceには仮想DOMツリー上の親要素に当たるインスタンスが入っています。例えば、Layer直下にCircleを追加した場合、 parentInstance はLayer、 child にはCircleが入ります。
まず、 child_remove() していますが、この insertBefore() は、仮想DOMが追加されるだけでなく、要素の順番を入れ替える変更が起きたときにも呼ばれるためです。
そして、 parentInstance.add(child) することでCanvasに描画させます。このadd()関数は、通常のKonnva.jsでも使用する関数です。
また、 child.setZIndex(beforeChild.getZIndex()) で、直前の子要素より上に描画されるように、Canvas内での描画順序を変更しています。
最後に、updatePicture()が呼ばれていますが、以下のような関数となっています。

Konva.autoDrawEnabled がfalseの場合は、レイヤーに対する変更をしても再描画されないため、 drawingNode.batchDraw() を呼び出して実際にCanvasに描画するようにしています。
ただし、デフォルトで Konva.autoDrawEnabled はtrueなので、レイヤーに対する変更は自動的に描画されるようになっています。

さいごに

Reactの低レイヤー部分をカスタム(特にレンダリング部分)したコードでしたが、react-reconcilerを使っていることでシンプルな実装となっていることが分かり、Reactの拡張性の高さを実感しました。

おすすめ書籍

Reactハンズオンラーニング 第2版 ―Webアプリケーション開発のベストプラクティス プログラミングTypeScript ―スケールするJavaScriptアプリケーション開発

blog-page_footer_336




blog-page_footer_336




-FrontEnd
-,

執筆者:


comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


関連記事

正式版Vue.js 3.0のTeleportを触ってみる

1 はじめに1.1 Vue.js 3.0のプロジェクト作成方法1.2 Teleportとは2 基本形2.1 コード2.2 画面3 別のコンポーネントの入れ子にする場合3.1 コード3.2 画面4 同じ ...

JQueryでformにinput要素を足していく

はじめに 最近個人的にディープラーニングの勉強をしているtonnyです。 ディープラーニングの勉強がてら、好きな麻雀に関するWebアプリを作成してみました。 今回はその作成の中で、今まであまりやってこ ...

もうすぐ登場!Vue 3の変更点まとめ

1 はじめに2 仕様変更2.1 複数のv-modelが定義可能に2.2 template直下に複数のタグを記述可能に2.3 開始処理がcreateAppに2.4 scoped cssの仕様変更2.5 ...

ウチのMaterialize事情

1 はじめに2 ボタン3 フォーム3.1 ラベルについて3.2 セレクトボックスについて3.3 ラジオボタンについて4 アラート5 フォント6 さいごに はじめに うちのチームでは現在、CSSフレーム ...

Vue 3とVuex 4とTypeScriptでタイプセーフに開発する

1 はじめに1.1 インストール1.2 Storeの設定2 Storeの作成3 StoreをComponentから使用する4 $storeプロパティに型をつける5 さいごに6 おすすめ書籍 はじめに ...

フォロー

blog-page_side_responsive

2022年10月
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

アプリ情報

私たちは無料アプリもリリースしています、ぜひご覧ください。 下記のアイコンから無料でダウンロードできます。