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


関連記事

rails

Rails 7でフロントエンド開発が大きく変わる

1 はじめに1.1 脱Node.jsの経緯2 Rails 7.0でのアセット管理3 propshaft4 importmap-rails4.1 JavaScript CDN経由でのnpmパッケージの利 ...

Vue.js 3.0のComposition APIを使ってみた

1 はじめに1.1 Composition APIとは1.2 環境構築2 Composition API での書き方2.1 function2.2 computed2.3 watch2.4 lifec ...

Vue.jsのコードをTypeScriptで書く

1 はじめに2 TypeScriptでの書き方2.1 定義2.2 data2.3 methods2.4 computed2.5 props2.6 emit2.7 lifecycle hooks2.8 ...

Nuxt 3(ベータ)の新機能を紹介

1 はじめに2 Nuxt 3の特徴2.1 新しいサーバエンジン(Nitro Engine)が登場2.2 Vue 3&Composition APIのネイティブサポート2.3 TypeScriptのネイ ...

【Vue.js】コンポーネントのテストコードをvue-test-utilsとJestで実装

1 はじめに2 セットアップ3 コンポーネントのテスト3.1 Shallow 描画3.2 基本的な例3.3 プロパティを渡す3.4 ユーザーの操作をシミュレーションする3.5 イベントを検証する3.6 ...

フォロー

blog-page_side_responsive

2022年10月
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

アプリ情報

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