木木剑光

mmjg

Full Stack Developer | Ant design Contributor

Vaporモード研究計画前奏:アーキテクチャの進化史の浅い分析

Vapor モードとは#

Vapor モードの中国語直訳は蒸気モードです。

これは Vapor の GitHub リポジトリからの説明の一部です。

image

Vapor モードの開発のために、Vue チームは Vue3 のメインブランチからフォークした新しいリポジトリ「core-vapor」を作成しました。目的は Vue の仮想 DOM なしのレンダリングモードを実現することです。

ここでVueの発展過程に詳しい方は、**「Vue1.0」は仮想 DOM のないバージョンではないか、これだけの年月が経ってどうして戻るのかと疑問に思うかもしれません。ここで、Vue の各大バージョンの「アーキテクチャの進化史」** を整理する必要があります。

Vue 1.0 量子もつれの細粒度バインディング#

これは Vue の初期のマイルストーンであり、この時点では完全なリアクティブシステムは存在していませんでした。この時期に最も多く語られたのは、データハイジャック + 依存収集に基づくリアクティブ実装の提案と、実装の詳細におけるWatcherDepの二兄弟です。

理解していない方はこの「レンダリングフローチャート」を見てみると、Vue1.0 バージョンに対する理解を迅速に構築するのに役立ちます。

image

まだSFC(単一ファイルコンポーネント)が存在しない時期に、Vue は実質的な **「ランタイム」** フレームワークと言えます。上の図は、**new Vue ({...})** を実行した後に発生する一連のプロセスを簡潔に示しています。

  • definePropertyを使用してデータハイジャックを実現し、データを代理し、すべてのデータに対するgetおよびset操作をインターセプトします。このプロセスは「getter/setter」化とも呼ばれます。

  • プロパティの「getter」の主な役割は依存を収集することです。ここでの「依存」とは、そのプロパティにアクセスするすべてのロジックを指し、計算プロパティやリスナー、テンプレート内のディレクティブや挿入式などが含まれます。実装の観点から、これらの異なる依存は統一された概念であるWatcherに抽象化され、各プロパティのDepインスタンスに保存されます。

  • プロパティの「setter」は、データが変更されたときにDepに収集されたWatcherに通知してビューを更新します。

全体のプロセスにおいて、Vue の依存収集の **「粒度」は非常に細かく、「リアクティブ」データにアクセスするすべての場所が「依存」** として収集されます。

上位の観点から見ると、「データ」と対応する「UI」がバインディング関係を形成し、これが Vue が「ビューを更新」する際に非常に高い効率を持つ根本的な理由です。このバインディング関係は量子もつれのようで、**「データ」が変化すると同時に「UI」** が通知を受けて更新を行います。

しかし、すべての事物には二面性があり、過度に細かい依存収集は利点でもあり短所でもあります。プロジェクトの規模が大きくなるにつれて、ランタイムはますます多くのWatcherDepを生成し、過剰なメモリを消費し、ページのパフォーマンスに影響を与えます。

もしある提案が小規模なプロジェクトに対して良好な **「パフォーマンス」** を提供できるだけであれば、明らかに「最適解」ではありません。

Vue 2.0 依存収集の粒度を調整し、仮想 DOM を導入#

大規模プロジェクトでより良いパフォーマンスを発揮するために、Vue2.0 バージョンは大きな調整を行いました。以下のフローチャートから、Vue のアーキテクチャ体系に直感的な変化があったことがわかります。

  • 1.0バージョンに比べてコンポーネントの概念が登場しました。
  • 依存収集の粒度を「コンポーネントレベル」に調整し、1 つのコンポーネントが 1 つのWatcherとなります。
  • 仮想 DOMが導入され、レンダリングプロセスの非常に重要な部分となりました。

image

全体的に見ると、「リアクティブ」データはもはやコンポーネント内部の「依存」に注目せず、データが変更されたときにはコンポーネントに通知され、コンポーネントはdiffアルゴリズムを使用して「仮想 DOM」の変更部分を特定し、その部分を「リアル DOM」に更新します。これは本質的に時間を空間に換えるというトレードオフであり、適切に **「ランタイム」「更新効率」を低下させることで、より少ない「メモリオーバーヘッド」** を得ることを意味します。

同時期に Vue はSFC(.vue ファイル)も新たに追加しました。.vueファイルはフレームワークが提供する“魔法”であり、ブラウザに直接実行させることはできないため、.vueを **.jsに変換することができるツールが必要です。このツールの核心はコンパイラ(compiler)であり、魔法を打破するプロセスはコンパイル(compile)と呼ばれます。つまり、コンパイルを経て、.vueファイル内で書かれたv-if, v-for, 挿入式などの特性は普通のjavascriptロジックに変換されます。templateはこの段階でrender** 関数に変換されます。

image

1.0 バージョンにもcompileプロセスは存在しましたが、行われることは大きく変わりました。

compile1.02.0
ステージランタイムコンパイル時
役割テンプレート内のディレクティブ、挿入などを解析して対応するロジックに変換し実行テンプレートを抽象構文木(AST)に解析し、レンダリング関数を生成

簡単な比較を通じて、コンパイルの実行ステージは完全に異なっているにもかかわらず、テンプレートの解析が主旋律であることがわかります。

実際、ここまで来ると、Vue はアーキテクチャの面で極限の最適化を達成したと感じませんか?焦らずに、Vue 3.0 バージョンの進化を見た後に結論を出しましょう。

Vue 3.0 コンポジション API#

typescriptのフロントエンドでの台頭と、プロキシが主要なブラウザでサポートされるようになったことで、Vue3は完全に再構築された姿で登場しました。

同時に、オプションAPIのロジックが分散しすぎて、開発者が高い凝集性のあるコードを書くのが難しいという痛点を解決するために、React hooksの考え方に触発されて、Vue3 もコンポジション APIを導入しました。期間中にはmixinのような提案もありましたが、いずれも多かれ少なかれ欠点がありました。

コンポジション APIモードでは、hooks を利用してビジネス機能をうまく組み合わせ、ロジックの凝集を実現できます。

新機能に加えて、Vue は細部の磨き上げにも多くの最適化を行いました。例えば、

  • 事前文字列化
  • 静的ノードの昇格
  • パッチフラグ

注意深い方は、これらの最適化がコンパイル時の最適化であり、ランタイムの最適化ではないことに気づいているでしょう。なぜなら、ランタイムでは最適化の方向性があまりなく、基本的にはパッチプロセスのdiffアルゴリズムに関連しているからです。

もう一度 Vue vapor について#

Vue の進化の過程を振り返った後、私たちはテーマであるVapor モードに戻ることができます。これは Vue がコンパイル時の最適化に関する研究を行っているものであり、もちろんSolidJSからも影響を受けています。

前述のように、Vue のアーキテクチャ体系に基づいて、ランタイムの最適化はほぼ仮想 DOMの変更部分をより早く見つけることに関するものだけです。

もし Vue1.0 の仮想 DOMが存在しなかった場合、その時の細粒度バインディングは変更を探す必要がなく、仮想 DOM も必要ありませんでしたが、痛点は以下の通りです。

  • 依存はランタイムで決定される。システムの初期化にはコンパイル段階でディレクティブなどを解析し依存を収集する必要があり、ファーストスクリーンのレンダリング時間が増加します。

  • 細粒度の依存収集は大量のWatcherを生成し、より多くのメモリオーバーヘッドをもたらします。

今、別の視点で考えてみましょう。依存の決定時期ランタイムからコンパイル時に移すことで、コンパイル手段を用いて各リアクティブデータの変化時に実行する必要がある更新ロジックを生成すれば、上記の最初の痛点を解決できるのではないでしょうか。これがVapor モードの考え方です。

第二の痛点については、理論的には依存収集の粒度が粗から細に移行することで、必然的にメモリオーバーヘッドが増加します。この点についてVapor モードはどのように対処するのか、私たちは疑問を持ち、正式にリリースされた後にコールバックを待ちましょう。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。