blog

コンポーネントレンダリングのVue3.0ソースコード解析、実DOMへのvnode

このコードはページ上にラベルをレンダリングしませんが、何がレンダリングされるかはコンポーネントのテンプレートの書き方次第です。たとえば、コンポーネント内部のテンプレート定義は次のようになります。ご覧の...

Jun 19, 2020 · 18 min. read
シェア

コンポーネントはVue.jsにおいて非常に重要な概念であり、アプリケーションのページ全体はコンポーネントによってレンダリングされますが、これらのコンポーネントを記述する際に内部的にどのように動作するかご存知でしょうか?コンポーネントの記述から実際のDOMへの遷移はどのようになっているのでしょうか?

まず、コンポーネントとはDOMツリーを抽象化したもので、ページ内にコンポーネントノードを記述します:

<hello-world></hello-world>

このコードはページにラベルをレンダリングしませんが、正確に何をレンダリングするかは、HelloWorldコンポーネントのテンプレートをどのように書くかによります。例として、HelloWorld コンポーネント内のテンプレート定義は次のようになります:

<template>
 <div>
 <p>Hello World</p>
 </div>
</template>

ご覧のように、テンプレートは内部的に、Hello Worldのテキストを表示するpタグを含むdivをページ上にレンダリングすることになります。

つまり、プレゼンテーションの面では、コンポーネントのテンプレートによって、コンポーネントが生成するDOMタグが決まりますが、Vue.jsの内部では、コンポーネントが実際にDOMをレンダリングするためには、「vnodeの作成 - vnodeのレンダリング - DOMの生成」というステップを踏む必要があります:

vnodeとは何なのか、コンポーネントとどのような関係があるのか?後で詳しく説明しますのでご心配なく。ここでは、コンポーネント情報を記述できるJavaScriptオブジェクトであることを覚えておいてください。

次に、Vue.js 3.0でコンポーネントがどのようにレンダリングされるかを、アプリケーションのエントリーポイントから順を追って見ていきます。

アプリケーションの初期化

コンポーネントは "テンプレート+オブジェクト記述 "で作成できますが、作成後、どのように呼び出され、初期化されるのでしょうか?コンポーネントツリー全体はルートコンポーネントからレンダリングされるので、ルートコンポーネントのレンダリングエントリポイントを見つけるためには、アプリケーションの初期化プロセスから分析する必要があります。

ここでは、Vue.js 2.xとVue.js 3.0をそれぞれ使ってアプリケーションを初期化するコードを示します:

// Vueで.js 2.x アプリケーションは次のように初期化される。 import Vue from 'vue' import App from './App' const app = new Vue({ render: h => h(App) }) app.$mount('#app') // Vueで.js 3.0 アプリケーションは次のように初期化される。 import { createApp } from 'vue' import App from './app' const app = createApp(App) app.mount('#app')

ご覧のように、Vue.js 3.0がアプリを初期化する方法は、Vue.js 2.xとそれほど変わりません。

しかし、Vue.js 3.0はcreateAppもインポートしています。これはVue.jsが外部に公開するエントリ関数なので、その内部実装を見てみましょう:

const createApp = ((...args) => { // アプリ・オブジェクトを作成する const app = ensureRenderer().createApp(...args) const { mount } = app // マウントメソッドを書き直す app.mount = (containerOrSelector) => { // ... } return app })

コードから、createAppが主に2つのことを行っていることがわかります。appオブジェクトの作成とapp.mountメソッドの書き換えです。次に、これらを分析してみましょう。

アプリオブジェクトの作成

まず、secureRenderer().createApp() でアプリオブジェクトを作成します:

 const app = ensureRenderer().createApp(...args)

ensureRenderer() を使用してレンダラー・オブジェクトを作成する場合の内部コードは次のようになります:

// プロパティの更新やDOMを操作するメソッドなど、レンダリング関連の設定。 const rendererOptions = { patchProp, ...nodeOps } let renderer // レンダラーの遅延作成は、ユーザーがレスポンシブ・パッケージのみに依存する場合、ツリーシェイクによってコアレンダリングロジックに関連するコードを削除することができる。 function ensureRenderer() { return renderer || (renderer = createRenderer(rendererOptions)) } function createRenderer(options) { return baseCreateRenderer(options) } function baseCreateRenderer(options) { function render(vnode, container) { // コンポーネント・レンダリングのコア・ロジック } return { render, createApp: createAppAPI(render) } } function createAppAPI(render) { // createApp createApp メソッドは2つのパラメータを受け取る。 return function createApp(rootComponent, rootProps = null) { const app = { _component: rootComponent, _props: rootProps, mount(rootContainer) { // ルート・コンポーネントのvnodeを作成する const vnode = createVNode(rootComponent, rootProps) // レンダラーでvnodeをレンダリングする render(vnode, rootContainer) app._container = rootContainer return vnode.component.proxy } } return app } }

これは、ユーザーがレスポンシブパッケージのみに依存する場合、レンダラーが作成されないという利点があるため、コアレンダリングロジックに関連するコードをツリーシェイクによって削除できます。

ここでは、クロス プラットフォーム レンダリングに備えてレンダラーの概念を説明しますが、これについては後のカスタム レンダラーのセクションで詳しく説明します。このコンテキストでは、レンダラーを単純に、プラットフォーム レンダリングのコア ロジックを含む JavaScript オブジェクトとして理解できます。

上記のコードを続けると、Vue.js 3.0内部では、createRendererを介してレンダラーが作成され、このレンダラー内部には、createAppAPIメソッドの実行から返される関数であるcreateAppメソッドがあり、rootComponentとrootPropsパラメータを受け取り、アプリケーションレベルでcreateApp(App)メソッドが実行されると、AppコンポーネントオブジェクトがルートコンポーネントとしてrootComponentに渡されます。createApp(App)メソッドがアプリケーションレベルで実行されると、AppコンポーネントオブジェクトがルートコンポーネントとしてrootComponentに渡されます。createAppはその後、内部でappオブジェクトを作成し、コンポーネントをマウントするために使用されるmountメソッドを提供します。

Vue.jsは、アプリオブジェクトの作成プロセスを通じて、クロージャと関数の粒度をうまく利用してパラメータの保持を実現しています。たとえば、app.mount を実行するときにレンダラー render を渡す必要はありません。createAppAPI を実行するときにレンダラー render パラメータがすでに保持されているからです。

アプリの書き換え.mount

次のステップは、app.mountメソッドをオーバーライドすることです。

先ほどの分析から、createAppによって返されたappオブジェクトはすでにmountメソッドを持っていることがわかりますが、エントリー関数の次のロジックはapp.mountメソッドのオーバーライドです。なぜアプリのmountメソッド内にロジックを置くのではなく、このメソッドをオーバーライドするのでしょうか?

なぜなら、Vue.jsはウェブだけでなく、クロスプラットフォームのレンダリングをサポートするように設計されており、createApp関数内のapp.mountメソッドは、標準的なクロスプラットフォームのコンポーネントのレンダリング処理だからです:

mount(rootContainer) {
 // ルート・コンポーネントのvnodeを作成する
 const vnode = createVNode(rootComponent, rootProps)
 // レンダラーでvnodeをレンダリングする
 render(vnode, rootContainer)
 app._container = rootContainer
 return vnode.component.proxy
}

標準的なクロスプラットフォームのレンダリングプロセスは、最初にvnodeを作成し、次にvnodeをレンダリングします。パラメータrootContainerは、WebプラットフォームではDOMオブジェクトなど、他のプラットフォームでは異なるタイプの値にすることができます。つまり、このコードの実行ロジックはプラットフォームに依存しません。そこで、このメソッドを外部で書き換えて、Webプラットフォームでのレンダリングロジックを改善する必要があります。

次に、app.mountの書き換えが何をするのかを見てみましょう:

app.mount = (containerOrSelector) => {
 // 標準化されたコンテナ
 const container = normalizeContainer(containerOrSelector)
 if (!container)
 return
 const component = app._component
 // コンポーネント・オブジェクトがレンダリング関数とテンプレートを定義していない場合は、コンテナのinnerHTMLをコンポーネント・テンプレートのコンテンツとして使用する。
 if (!isFunction(component) && !component.render && !component.template) {
 component.template = container.innerHTML
 }
 // 取り付け前にコンテナの中身を空にする
 container.innerHTML = ''
 // リアルマウント
 return mount(container)
}

まず、normalizeContainerによってコンテナを標準化し、次にif判定を行い、コンポーネント・オブジェクトがレンダー関数とテンプレート・テンプレートを定義していない場合は、コンテナのinnerHTMLをコンポーネント・テンプレートのコンテンツとします。プロセスを実行します。

ここでは、書き換えたロジックはすべてウェブプラットフォームに関連するものなので、外部に実装しています。加えて、APIをより柔軟に利用できるようにするためと、Vue.js 2.xの記述スタイルと互換性を持たせるためです。例えば、app.mountの最初のパラメータは、セレクタ文字列とDOMオブジェクト型の両方をサポートしています。

app.mountから、コンポーネントのレンダリング処理に入ります。そこで、コアとなるレンダリング処理が行う2つのこと、vnodeの作成とvnodeのレンダリングに注目してみましょう。

コアレンダリングプロセス:vnodeの作成とvnodeのレンダリング

vnodeの作成

まず、vnodeを作成するプロセスがあります。

vnodeは基本的にDOMを記述するためのJavaScriptオブジェクトで、Vue.jsでは通常の要素ノードやコンポーネントノードなど、さまざまな種類のノードを記述できます。

通常の要素ノードとは?例として、ボタンをタグを使ってHTMLで記述します:

<button class="btn" style="width:100px;height:50px">click me</button>

このようにvnodeを使って表すことができます

const vnode = {
 type: 'button',
 props: { 
 'class': 'btn',
 style: {
 width: '100px',
 height: '50px'
 }
 },
 children: 'click me'
}

type属性はDOMのタグ・タイプを表し、props属性はスタイルやクラスなどのDOMの追加情報を表し、children属性はDOMの子ノードを表します。

コンポーネント・ノードとは何ですか?実は、vnodeは、上記のような実際のDOMに加えて、コンポーネントを記述するために使用することができます。

まず、テンプレートにコンポーネント・タグを導入します:

<custom-component msg="test"></custom-component>

コンポーネント・ラベルを表現するには、vnodeを次のように使用します:

const CustomComponent = {
 // ここでコンポーネント・オブジェクトを定義する
}
const vnode = {
 type: CustomComponent,
 props: { 
 msg: 'test'
 }
}

コンポーネントのvnodeは、実際にはページ上のタグをレンダリングするのではなく、コンポーネントの内部で定義されたHTMLタグをレンダリングするので、実際には抽象的なものの説明です。

上記の2種類のvnodeの他に、プレーンテキストvnode、注釈vnodeなどがありますが、研究の本筋はコンポーネントvnodeと通常の要素vnodeを研究すればよいので、ここでは繰り返しません。

また、Vue.js 3.0では、サスペンス、テレポートなど、vnodeのタイプをより詳細に分類し、vnodeのタイプ情報をエンコードすることで、後のパッチフェーズで、タイプに応じた処理ロジックを実行できるようになりました:

const shapeFlag = isString(type)
 ? 1 /* ELEMENT */
 : isSuspense(type)
 ? 128 /* SUSPENSE */
 : isTeleport(type)
 ? 64 /* TELEPORT */
 : isObject(type)
 ? 4 /* STATEFUL_COMPONENT */
 : isFunction(type)
 ? 2 /* FUNCTIONAL_COMPONENT */
 : 0

vnodeが何であるかを知ると、vnodeの利点は何だろう?なぜvnodeのようなデータ構造を設計する必要があるのでしょうか?

1つ目は抽象化で、vnodeの導入によりレンダリングプロセスが抽象化され、コンポーネントの抽象化も向上しました。

第二に、vnodeにパッチを適用するプロセスが異なるプラットフォーム上で独自の実装を持つことができるため、クロスプラットフォームであり、vnodeをベースにしたサーバサイドレンダリング、Weexプラットフォーム、アプレットプラットフォームレンダリングを行うことが非常に容易になります。

多くの学生はvnodeのパフォーマンスがネイティブDOMを手動で操作するよりも優れていると誤解していますが、必ずしもそうではありません。

なぜなら、まず第一に、このMVVMフレームワークは、vnodeの実装に基づいて、毎回vnodeにレンダリングする過程で、JavaScriptの一定量の時間のかかるレンダリングコンポーネント、特に1000 * 10テーブルコンポーネントのような大規模なコンポーネントがあるでしょう、vnodeにレンダリングするプロセスは、内部のセルを作成するために1000 * 10回をトラバースします。vnode、全体の時間のかかるプロセスが長くなり、さらにパッチvnodeのプロセスも一定の時間のかかるプロセスを持って、コンポーネントを更新しに行くとき、ユーザーは明らかなラグを感じるでしょう。diffアルゴリズムはDOM操作の回数を減らすには十分ですが、最終的にはDOMの操作を避けることはできません。

では、Vue.jsは内部でどのようにvnodeを作成しているのでしょうか?

app.mount関数の実装を思い出すと、内部的にはルートコンポーネントのvnodeはcreateVNode関数で作成されます:

 const vnode = createVNode(rootComponent, rootProps)

createVNode関数の一般的な実装を見てみましょう:

function createVNode(type, props = null ,children = null) { if (props) { // 小道具関連のロジックを扱い、クラスとスタイルを標準化する } // vnodeタイプ情報をエンコードする const shapeFlag = isString(type) ? 1 /* ELEMENT */ : isSuspense(type) ? 128 /* SUSPENSE */ : isTeleport(type) ? 64 /* TELEPORT */ : isObject(type) ? 4 /* STATEFUL_COMPONENT */ : isFunction(type) ? 2 /* FUNCTIONAL_COMPONENT */ : 0 const vnode = { type, props, shapeFlag, // その他のプロパティ } // 子ノードを標準化し、異なるデータ型の子を配列またはテキスト型に変換する normalizeChildren(vnode, children) return vnode }

上記のコードからわかるように、createVNodeが行うことは非常にシンプルです:propsを正規化し、vnodeのタイプ情報をエンコードし、vnodeオブジェクトを作成し、子を正規化します。

このvnodeオブジェクトができたので、次にすることは、それをページにレンダリングすることです。

vnodeのレンダリング

次に、vnodeをレンダリングするプロセスです。

app.mount関数の実装を思い出すと、内部的にこのコードは作成されたvnodeをレンダリングするために実行されます:

render(vnode, rootContainer)
const render = (vnode, container) => {
 if (vnode == null) {
 // コンポーネントを破棄する
 if (container._vnode) {
 unmount(container._vnode, null, null, true)
 }
 } else {
 // コンポーネントを作成または更新する
 patch(container._vnode || null, vnode, container)
 }
 // レンダリング済みであることを示すvnodeノードをキャッシュする
 container._vnode = vnode
}

render関数の実装は単純で、最初のパラメータvnodeがnullの場合はコンポーネントを破棄するロジックを実行し、そうでない場合はコンポーネントを作成または更新するロジックを実行します。

次に、上のvnodeレンダリングコードに含まれるパッチ関数の実装を見てみましょう:

const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => { // 古いノードと新しいノードがあり、古いノードと新しいノードのタイプが異なる場合は、古いノードを破棄する。 if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null } const { type, shapeFlag } = n2 switch (type) { case Text: // テキストノードを扱う break case Comment: // 注釈付きノードを処理する break case Static: // 静的ノードを扱う break case Fragment: // フラグメント要素を扱う break default: if (shapeFlag & 1 /* ELEMENT */) { // 通常のDOM要素を扱う processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) } else if (shapeFlag & 6 /* COMPONENT */) { // コンポーネントを処理する processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) } else if (shapeFlag & 64 /* TELEPORT */) { // テレポートの処理 } else if (shapeFlag & 128 /* SUSPENSE */) { // SUSPENSEを処理する } } }

この関数には2つの機能があり、1つはvnodeに従ってDOMをマウントすること、もう1つは古いvnodeと新しいvnodeに従ってDOMを更新することです。最初のレンダリングについては、ここでは作成プロセスのみを分析し、更新プロセスについては後のセクションで分析します。

パッチ関数は作成時にいくつかの引数を取ります:

  1. 最初のパラメータn1は古いvnodeを表し、n1がNULLの場合は1回限りのマウント処理であることを意味します;
  2. 2番目のパラメータn2は新しいvnodeノードを表し、vnodeタイプによって異なる処理ロジックが実行されます;
  3. つまり、vnode はレンダリング後にコンテナの下にマウントされ、DOM が生成されます。レンダリングされたノードについては、コンポーネントの処理と通常の DOM 要素の処理という 2 種類のノードのレンダリングロジックに注目します。

コンポーネントの処理から見ていきましょう。最初のレンダリングはコンポーネントvnodeであるAppコンポーネントなので、コンポーネント処理ロジックがどのように動作するかを見てみましょう。まず、コンポーネントの処理に使用されるprocessComponent関数の実装です:

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
 if (n1 == null) {
 // コンポーネントをマウントする
 mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
 }
 else {
 // コンポーネントを更新する
 updateComponent(n1, n2, parentComponent, optimized)
 }
}

この関数のロジックは単純で、n1がNULLの場合はコンポーネントをマウントするロジックを実行し、そうでない場合はコンポーネントを更新するロジックを実行します。

コンポーネントをマウントするmountComponent関数の実装に移りましょう:

const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
 // コンポーネントの作成例
 const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
 // コンポーネントの設定例
 setupComponent(instance)
 // 副作用のあるレンダー関数を設定して実行する
 setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}

ご覧のように、mountComponent関数は主に3つのことを行います。コンポーネントインスタンスの作成、コンポーネントインスタンスのセットアップ、そして副作用を伴うレンダー関数のセットアップと実行です。

Vue.js 3.0は、Vue.js 2.xのようにクラスを介してコンポーネントをインスタンス化しませんが、内部的にはオブジェクトを介して現在レンダリングされているコンポーネントのインスタンスを生成します。

次に、コンポーネントインスタンスのセットアップです。インスタンスは多くのコンポーネント関連データを保持し、インスタンスのプロップ、スロット、その他のプロパティの初期化を含め、コンポーネントのコンテキストを維持します。

コンポーネント・インスタンスの作成と設定の2つのプロセスについては、ここでは取り上げず、後の章で詳しく分析します。

最後のステップは、副作用のあるレンダー関数setupRenderEffectを実行することで、この関数の実装に焦点を当てます:

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => { // レスポンシブな副作用レンダリング関数を作成する instance.update = effect(function componentEffect() { if (!instance.isMounted) { // サブツリーを生成するコンポーネントのレンダリング vnode const subTree = (instance.subTree = renderComponentRoot(instance)) // サブツリーvnodeをコンテナにマウントする patch(null, subTree, container, anchor, instance, parentSuspense, isSVG) // レンダリングによって生成されたサブツリーのルートDOMノードを保持する initialVNode.el = subTree.el instance.isMounted = true } else { // コンポーネントを更新する } }, prodEffectOptions) }

この関数は、レスポンシブライブラリの effect 関数を使用して、副作用のあるレンダリング関数 componentEffect を作成します。ここで簡単に理解できる副作用とは、コンポーネントのデータが変更されたときに、effect関数にラップされた内部のレンダリング関数componentEffectが再実行され、コンポーネントが再レンダリングされることです。

また、レンダー関数は、内部的に初期レンダーかコンポーネント更新かを判断します。ここでは、初期レンダリング処理のみを分析します。

初期レンダリングでは、レンダリング コンポーネントがサブツリーを生成することと、サブツリーがコンテナにマウントされることの 2 つが行われます。

最初に、レンダリング コンポーネントがsubTreeを生成します。subTreeとinitialVNodeを混同しないように注意してください。親AppにHelloコンポーネントが導入されている例で説明します:

<template>
 <div class="app">
 <p>This is an app.</p>
 <hello></hello>
 </div>
</template>

Helloコンポーネントでは

ラベルは

タグ

<template>
 <div class="hello">
 <p>Hello, Vue 3.0!</p>
 </div>
</template>

App コンポーネントでは、ノードレンダリングによって生成される vnode は Hello コンポーネントの initialVNode であり、記憶のために「コンポーネント vnode」と呼ぶこともできます。Hello コンポーネント内の DOM ノード全体に対応する vnode は、renderComponentRoot によってレンダリングされる subTree で、「サブツリー vnode」と呼ぶこともできます。

各コンポーネントには対応するレンダー関数があり、テンプレートを書いてもレンダー関数にコンパイルされ、renderComponentRoot 関数は、レンダー関数を実行してコンポーネントツリー全体の内部に vnode を作成し、内部レイヤーを通して vnode を標準化することで、関数の戻り結果が得られます。サブツリーの vnode。

サブツリー vnode をレンダリングした後、次のステップでは patch 関数を呼び出してサブツリー vnode をコンテナにマウントします。

そして、再びパッチ関数に戻り、サブツリーvnodeの型を判定し続けます。 上記の例では、アプリコンポーネントのルートノードは

上記の例では、アプリコンポーネントのルートノードはラベルなので、対応するサブツリー vnode も普通の要素 vnode です。

まず、通常の DOM 要素を処理する processElement 関数の実装を見てみましょう:

const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
 isSVG = isSVG || n2.type === 'svg'
 if (n1 == null) {
 //要素ノードをマウントする
 mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
 }
 else {
 //要素ノードを更新する
 patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
 }
}

この関数のロジックは非常に単純で、n1 が NULL の場合は要素ノードをマウントし、そうでない場合は要素ノードを更新します。

次に、要素をマウントする mountElement 関数の実装を見てみましょう:

const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
 let el
 const { type, props, shapeFlag } = vnode
 // DOM要素ノードを作成する
 el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
 if (props) {
 // クラス、スタイル、イベント、その他の属性などのプロップを扱う
 for (const key in props) {
 if (!isReservedProp(key)) {
 hostPatchProp(el, key, null, props[key], isSVG)
 }
 }
 }
 if (shapeFlag & 8 /* TEXT_CHILDREN */) {
 // 子ノードがプレーンテキストの場合の処理
 hostSetElementText(el, vnode.children)
 }
 else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
 // 子ノードが配列の場合の処理
 mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
 }
 // 作成したDOM要素ノードをコンテナにマウントする
 hostInsert(el, container, anchor)
}

ご覧のように、mountElement関数は主に4つのことを行います:DOM要素ノードの作成、propsの処理、childrenの処理、そしてコンテナへのDOM要素のマウントです。

最初に行うことは DOM 要素ノードの作成で、これは hostCreateElement メソッドによって作成されます。hostCreateElement メソッドはプラットフォーム依存のメソッドなので、Web 環境でどのように定義されているかを見てみましょう:

function createElement(tag, isSVG, is) { isSVG ? document.createElementNS(svgNS, tag) : document.createElement(tag, is ? { is } : undefined) }

そのため、基本的にVue.jsはDOMを操作しないことを強調しています。ユーザがDOMに直接触れないことを望んでいるだけで、魔法を使うわけではありません。

一方、Weex などの他のプラットフォームでは、hostCreateElement メソッドは DOM を操作せず、レンダラー作成フェーズでパラメータとして渡されるプラットフォーム関連の API を操作します。

DOM ノードを作成した後、次に行うことはプロップがあるかどうかを判断し、関連するクラス、スタイル、イベント、その他の属性を DOM ノードに追加し、関連する処理を行うことです。これらのロジックは hostPatchProp 関数内で行われるので、ここでは説明しません。

次に子ノードの処理ですが、DOMがツリーであることを知っていれば、vnodeもツリーであり、1つずつDOM構造にマッピングされます。

子ノードがプレーンテキストの場合、hostSetElementTextメソッドが実行され、Web環境のDOM要素のtextContent属性を設定してテキストを設定します:

function setElementText(el, text) {
 el.textContent = text
}

子ノードが配列の場合、mountChildren メソッドが実行されます:

const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) => {
 for (let i = start; i < children.length; i++) {
 // 前処理の子
 const child = (children[i] = optimized
 ? cloneIfMounted(children[i])
 : normalizeVNode(children[i]))
 // 再帰パッチマウント子
 patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
 }
}

子ノードが配列の場合、mountChildren メソッドが実行されます。子ノードをマウントするためのロジックも非常に単純で、子ノードを繰り返し実行して各子ノードを取得し、patch メソッドを再帰的に実行して各子ノードをマウントします。子プロセスが前処理される場合があることに注意してください。

見ての通り、mountChildren 関数の2番目の引数はコンテナであり、mountChildren メソッドの2番目の引数は mountElement が呼ばれた時に生成された DOM ノードであり、親子関係をうまく確立しています。

さらに、深さ優先で再帰的にパッチを適用することで、完全な DOM ツリーを構築し、コンポーネントのレンダリングを完了することができます。

すべての子ノードが処理された後、作成された DOM 要素ノードは最終的に hostInsert メソッドによってコンテナにマウントされます:

function insert(child, parent, anchor) {
 if (anchor) {
 parent.insertBefore(child, anchor)
 }
 else {
 parent.appendChild(child)
 }
}

insert は子ノードが処理された後に実行されるため、マウントの順序は子ノードが最初で、次に親ノード、最後に一番外側のコンテナになります。

拡張: ネストされたコンポーネント

注意深く見ていると、mountChildrenがマウントされたとき、mountElement関数の代わりにpatch関数が再帰的に実行されることに気づくかもしれません。 これは、子ノードがコンポーネントvnodeなど他のタイプのvnodeを持つ可能性があるためです。

実際の開発シナリオでは、ネストされたコンポーネントシナリオは完全に正常であり、App と Hello コンポーネントの前述の例はネストされたコンポーネントシナリオです。コンポーネントvnodeは主にコンポーネントの定義オブジェクトとコンポーネント上の様々なpropsを保持し、コンポーネント自体は抽象ノードですが、それ自身のレンダリングは実際にはコンポーネント定義のrender関数を実行して、生成されたサブツリーvnodeをレンダリングして完了し、次にパッチを適用することで行われます。この再帰的な方法によって、コンポーネントのネストレベルがどんなに深くても、コンポーネントツリー全体のレンダリングが完了します。

最後に、全体のコンポーネントのレンダリングプロセスのより直感的な感じをもたらすために図:ネットワークから再現され、何か間違っている場合は、削除するために連絡してください!

Read next

JavaScriptにおけるクッキーの単純なカプセル化

cookieはカプセル化する必要があります。ネイティブのcookieインターフェースは十分にフレンドリーではないので、練習のために単純なバージョンのcookieをカプセル化しました。

Jun 19, 2020 · 3 min read