Vue3 AST パーサー - ソースコード分析

Vue3 AST パーサー - ソースコード分析

前回の記事「Vue3 のコンパイル プロセス - ソース コード分析」では、 packges/vue/src/index.tsのエントリから始めて、Vue オブジェクトのコンパイル プロセスについて学習しました。記事では、実行時にbaseCompile関数が AST 抽象構文木を生成することを説明しました。これは間違いなく重要なステップです。生成された AST を取得することによってのみ、AST のノードをトラバースして、 v-ifv-forなどのさまざまな命令を解析したり、ノードを分析して条件を満たすノードを静的に昇格したりするなどの変換操作を実行できるためです。これらはすべて、以前に生成された AST 抽象構文木に依存しています。そこで今日は、AST 解析を見て、Vue がテンプレートを解析する方法を見ていきます。

1. AST抽象構文木を生成する

まず、 baseCompile関数の ast のロジックとその後の使用法を確認しましょう。

エクスポート関数baseCompile(
  テンプレート: 文字列 | RootNode,
  オプション: CompilerOptions = {}
): コード生成結果 {

  /* 前のロジックを無視*/

  const ast = isString(テンプレート) ? baseParse(テンプレート、オプション) : テンプレート

  変身(
    アス、
    パラメータを無視する */
  )

  生成を返す(
    アス、
    拡張({}, オプション, {
      プレフィックス識別子
    })
  )
}

注意を払う必要のないロジックをコメントアウトしたので、関数本体のロジックは非常に明確になります。

  • astオブジェクトを生成する
  • astオブジェクトをパラメータとしてtransform関数に渡し、 astノードを変換します。
  • astオブジェクトをgenerate関数のパラメータとして渡し、コンパイルされた結果を返します。

ここでは主に ast の生成に焦点を当てます。 ast の生成には三項演算子の判断があることがわかります。渡されたtemplateテンプレート パラメータが文字列の場合、 baseParseが呼び出されてテンプレート文字列が解析され、それ以外の場合はtemplateが直接astオブジェクトとして使用されます。 ast を生成するためにbaseParseでは何が行われますか?ソースコードを見てみましょう。

エクスポート関数baseParse(
  内容: 文字列、
  オプション: ParserOptions = {}
): ルートノード {
  const context = createParserContext(content, options) // 解析コンテキスト オブジェクトを作成します const start = getCursor(context) // 解析プロセスを記録するためのカーソル情報を生成します return createRoot( // ルート ノードを生成して返します parseChildren(context, TextModes.DATA, []), // 子ノードをルート ノードの children 属性として解析します getSelection(context, start)
  )
}

各関数の役割を理解しやすくするために、 baseParse関数にコメントを追加しました。まず、解析コンテキストを作成し、そのコンテキストに基づいてカーソル情報を取得します。まだ解析は実行されていないため、カーソル内のcolumnlineoffset属性はすべてtemplateの開始位置に対応しています。次のステップは、ルート ノードを作成し、ルート ノードを返すことです。この時点で、ast ツリーが生成され、解析が完了します。

2. ASTのルートノードを作成する

エクスポート関数createRoot(
  子: TemplateChildNode[],
  loc = locスタブ
): ルートノード {
  戻る {
    タイプ: NodeTypes.ROOT、
    子供たち、
    ヘルパー: [],
    コンポーネント: [],
    ディレクティブ: [],
    ホイスト: [],
    インポート: [],
    キャッシュ済み: 0,
    気温: 0,
    codegenNode: 未定義、
    場所
  }
}

createRoot関数のコードを見ると、関数がRootNode型のルート ノード オブジェクトを返すことがわかります。このオブジェクトでは、渡した children パラメータがルート ノードのchildrenパラメータとして使用されます。これは非常に理解しやすいです。ツリーデータ構造として想像してみてください。したがって、ast 生成の重要なポイントはparseChildren関数に焦点が当てられます。 parseChildren関数のソースコードを見なくても、テキストから、子ノードを解析するための関数であることが大体わかります。次に、AST 解析で最も重要なparseChildren関数を見てみましょう。いつものように、理解しやすいように関数内のロジックを簡略化します。

3. 子ノードの解析

関数parseChildren(
  コンテキスト: ParserContext、
  モード: テキストモード、
  祖先: ElementNode[]
): テンプレート子ノード[] {
  const parent = last(ancestors) // 現在のノードの親ノードを取得します。const ns = parent ? parent.ns : Namespaces.HTML
  const nodes: TemplateChildNode[] = [] // 解析されたノードを保存 // ラベルが閉じられていない場合は、対応するノードを解析します while (!isEnd(context, mode, ancestors)) {/* ロジックを無視*/

  // 出力効率を向上させるために空白文字を処理します let removedWhitespace = false
  if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA){/* ロジックを無視*/

  // 空白文字を削除し、解析されたノード配列を返します。 return removedWhitespace ? nodes.filter(Boolean) : nodes
}

上記のコードから、 parseChildren関数が、 context : パーサー コンテキスト、 mode : テキスト データ型、 ancestors : 祖先ノード配列の 3 つのパラメーターを受け取ることがわかります。関数を実行すると、まず現在のノードの親ノードが祖先ノードから取得され、名前空間が決定され、解析されたノードを格納するための空の配列が作成されます。その後、タグの終了位置に到達したかどうかを判断するための while ループが実行されます。終了する必要のあるタグでない場合は、ループ本体でソース テンプレート文字列が分類され、解析されます。その後、空白文字を処理するロジックがあり、処理後に解析されたノード配列が返されます。 parseChildrenの実行フローについて大まかに理解できたので、関数の核心である while ループ内のロジックを見てみましょう。

while ステートメントでは、パーサーはテキスト データの種類を判別し、 TextModesが DATA または RCDATA の場合にのみ解析を続行します。

最初のケースは、Vue テンプレート構文の「 Mustache 」構文 (二重中括弧) を解析する必要があるかどうかを判断することです。現在のコンテキストに式をスキップする v-pre 命令がなく、ソース テンプレート文字列が指定した区切り文字で始まる場合 (この場合、 context.options.delimitersに二重中括弧が含まれています)、二重中括弧が解析されます。ここで、特別なニーズがあり、式の補間として二重中括弧を使用したくない場合は、コンパイルする前にオプションのdelimitersプロパティを変更するだけでよいことがわかります。

次に、最初の文字が「<」で、2 番目の文字が「!」の場合、コメント タグ,<!DOCTYPE<!CDATAを解析しようとします。3 つのケースでは、DOCTYPE は無視され、コメントとして解析されます。

次に、2 番目の文字が "/" の場合、"</" が終了タグの条件を満たしていると判断し、終了タグと一致させようとします。 3 番目の文字が ">" の場合、タグ名が欠落しているためエラーが報告され、パーサーは "</>" をスキップして 3 文字先に進みます。

「</」で始まり、3 番目の文字が小文字の英語の文字である場合、パーサーは終了タグを解析します。

ソース テンプレート文字列の最初の文字が "<" で、2 番目の文字が小文字の英語の文字で始まる場合、 parseElement関数が呼び出され、対応するタグが解析されます。

文字列の文字を判定する分岐条件が終了し、解析されたノードがない場合、ノードはテキスト型として扱われ、解析のために parseText が呼び出されます。

最後に、生成されたノードをnodes配列に追加し、関数の最後に返します。

これは while ループ内のロジックであり、 parseChildrenの最も重要な部分です。この判断プロセスでは、二重中括弧構文の解析、コメント ノードの解析方法、開始タグと終了タグの解析、テキスト コンテンツの解析を確認しました。簡略化されたコードは下のボックスにあります。ソースコードを理解するには、上記の説明を参照してください。もちろん、ソースコード内のコメントも非常に詳細です。

while (!isEnd(コンテキスト、モード、祖先)) {
  const s = コンテキスト.ソース
  ノード: TemplateChildNode | TemplateChildNode[] | undefined = undefined

  モード === TextModes.DATA || モード === TextModes.RCDATA) {
    コンテキスト.inVPre が s で始まり、コンテキスト.options.delimiters[0] が 0 の場合
      /* タグに v-pre ディレクティブがない場合、ソース テンプレート文字列は二重中括弧 `{{` で始まり、二重中括弧の構文に従って解析されます */
      ノード = parseInterpolation(コンテキスト、モード)
    } それ以外の場合 (mode === TextModes.DATA && s[0] === '<') {
      // ソーステンプレート文字列の最初の文字位置が `!` の場合
      s[1] === '!' の場合 {
    // '<!--' で始まる場合はコメントとして解析します if (startsWith(s, '<!--')) {
          ノード = parseComment(コンテキスト)
        } そうでない場合 (startsWith(s, '<!DOCTYPE')) {
     // '<!DOCTYPE' で始まる場合は、DOCTYPE を無視して疑似コメントとして解析します。node = parseBogusComment(context)
        } そうでない場合 (startsWith(s, '<![CDATA[')) {
          // '<![CDATA['で始まり、HTML環境にある場合は、CDATAを解析します
          if (ns !== Namespaces.HTML) {
            ノード = parseCDATA(コンテキスト、祖先)
          }
        }
      // ソーステンプレート文字列の2番目の文字位置が '/' の場合
      } そうでない場合 (s[1] === '/') {
        // ソーステンプレート文字列の3番目の文字位置が '>' の場合、それは自己終了タグであり、スキャン位置は3文字前方に移動します if (s[2] === '>') {
          出力エラー(コンテキスト、ErrorCodes.MISSING_END_TAG_NAME、2)
          advanceBy(コンテキスト, 3)
          続く
        // 3番目の文字位置が英語の文字の場合は、終了タグを解析します} else if (/[az]/i.test(s[2])) {
          parseTag(コンテキスト、TagType.End、親)
          続く
        } それ以外 {
          // 上記に当てはまらない場合は、疑似コメントとして解析します。node = parseBogusComment(context)
        }
      // タグの2番目の文字が小文字の英語文字の場合、要素タグとして解析されます} else if (/[az]/i.test(s[1])) {
        ノード = parseElement(コンテキスト、祖先)
        
      // 2番目の文字が '?' の場合、疑似コメントとして解釈します} else if (s[1] === '?') {
        ノード = parseBogusComment(コンテキスト)
      } それ以外 {
        // これらの条件がいずれも満たされない場合は、最初の文字が有効なラベル文字ではないことを示すエラー メッセージが表示されます。
        出力エラー(コンテキスト、エラーコード。タグ名の最初の文字が無効、1)
      }
    }
  }
  
  // 上記の状況を解析した後に対応するノードが作成されない場合は、テキストとして解析します if (!node) {
    ノード = parseText(コンテキスト、モード)
  }
  
  // ノードが配列の場合は、トラバースしてノード配列に追加し、そうでない場合は直接追加します if (isArray(node)) {
    (i = 0 とします; i < node.length; i++) {
      pushNode(ノード、ノード[i])
    }
  } それ以外 {
    pushNode(ノード、ノード)
  }
}

4. テンプレート要素の解析

whileループでは、各分岐判断ブランチで、 nodeさまざまなノード タイプの解析関数の戻り値を受け取ることがわかります。ここでは、テンプレートで最も頻繁に使用されるシナリオであるparseElement関数について詳しく説明します。

まずparseElementのソース コードを簡略化してここに貼り付け、次に内部のロジックについて説明します。

関数parseElement(
  コンテキスト: ParserContext、
  祖先: ElementNode[]
): ElementNode | 未定義 {
  // 開始タグを解析する const parent = last(ancestors)
  const 要素 = parseTag(コンテキスト、TagType.Start、親)
  
  // 自己終了タグまたは空タグの場合は、直接戻ります。 voidTag の例: `<img>`、`<br>`、`<hr>`
  if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
    戻り要素
  }

  // 子ノードを再帰的に解析する ancestors.push(element)
  定数モード = context.options.getTextMode(要素、親)
  const children = parseChildren(コンテキスト、モード、祖先)
  祖先.pop()

  要素.children = 子供

  // 終了タグを解析します if (startsWithEndTagOpen(context.source, element.tag)) {
    parseTag(コンテキスト、TagType.End、親)
  } それ以外 {
    出力エラー(コンテキスト、ErrorCodes.X_MISSING_END_TAG、0、要素.loc.start)
    context.source.length === 0 && element.tag.toLowerCase() === 'script' の場合 {
      定数 first = children[0]
      if (first && startsWith(first.loc.source, '<!--')) {
        出力エラー(コンテキスト、ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT)
      }
    }
  }
  // ラベル位置オブジェクトを取得します。element.loc = getSelection(context, element.loc.start)

  戻り要素
}

まず、現在のノードの親ノードを取得し、次にparseTag関数を呼び出して解析します。

parseTag 関数は次のプロセスに従って実行されます。

  • まずタグ名を一致させます。
  • 要素内の属性を解析し、props属性に格納します。
  • v-pre 命令があるかどうかを確認します。ある場合は、コンテキスト内の inVPre 属性を true に変更します。
  • 自己終了タグを検出します。自己終了タグの場合は、isSelfClosing プロパティを true に設定します。
  • タグタイプがELEMENT要素かCOMPONENTコンポーネントかSLOTスロットかを決定します。
  • 生成された要素オブジェクトを返します

elementオブジェクトを取得した後、 elementが <img>、<br>、<hr> などの自己終了タグか空タグかを判定します。空タグの場合は、 elementオブジェクトが直接返されます。

次に、 elementの子ノードを解析し、 elementをスタックにプッシュしてから、 parseChildrenを再帰的に呼び出して子ノードを解析します。

定数親 = 最後(祖先)

parseChildrenparseElementのコード行を振り返ってみると、 elementスタックにプッシュした後、取得する親ノードが現在のノードであることがわかります。解析が完了したら、 ancestors.pop()を呼び出して、現在子ノードが解析されているelementをポップし、解析されたchildrenオブジェクトをelementchildren属性に割り当てて、 elementの子ノードの解析を完了します。これは非常に巧妙な設計です。

最後に、終了タグを一致させ、要素の loc 位置情報を設定し、解析されたelementオブジェクトを返します。

5. 例: テンプレート要素の解析

以下に解析するテンプレートを示します。この図は、解析プロセス中に解析した後のノードのスタックのストレージを示しています。

<div>
  <p>こんにちは世界</p>
</div>

図の黄色の四角形はスタックです。解析が開始されると、 parseChildren最初に div タグを検出し、 parseElement関数の呼び出しを開始します。 div 要素は parseTag 関数を通じて解析され、スタックにプッシュされ、子ノードが再帰的に解析されます。 2 回目に parseChildren 関数が呼び出されると、p 要素が検出され、parseElement 関数が呼び出されて p タグがスタックにプッシュされます。この時点で、スタックには div と p の 2 つのタグがあります。 p 内の子ノードを再度解析し、 parseChildrenタグを 3 回目に呼び出します。今回は、一致するタグはなく、対応するノードは生成されません。そのため、parseText 関数を使用してテキストを生成し、ノードをHelloWorldとして解析し、ノードを返します。

このテキスト型node p タグの children 属性に追加した後、p タグの子ノードが解析され、祖先スタックがポップされ、終了タグが解析された後、p タグに対応するelementオブジェクトが返されます。

p タグに対応するノードが生成され、対応するノードがparseChildren関数で返されます。

p タグからノードを受け取った後、div タグはそれを自身の children 属性に追加し、スタックからポップします。この時点では、祖先スタックは空です。 div タグは閉じた解析ロジックを完了すると、 element要素を返します。

最後に、 parseChildrenの最初の呼び出しは結果を返し、div に対応するノード オブジェクトを生成し、結果も返します。この結果は、 createRoot関数の children パラメーターとして渡され、ルート ノード オブジェクトを生成して ast 解析を完了します。

Vue3 ASTパーサーのソースコード解析に関するこの記事はこれで終わりです。Vue3 AST パーサーに関するその他の関連コンテンツについては、123WORDPRESS.COM の以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • Vue3 コンパイルプロセス - ソースコード分析
  • Vue3における7種類のコンポーネント通信の詳細
  • Vue3カプセル化メッセージメッセージプロンプトインスタンス関数の詳細な説明
  • Vue2とVue3の兄弟コンポーネント通信バスの違いと使い方
  • vue3 を使用してカウント関数コンポーネントのカプセル化例を実装する
  • Vue3.0はドロップダウンメニューのカプセル化を実装します
  • Vue3.0はチェックボックスコンポーネントのカプセル化を実装します
  • vue3とvue2の利点の比較
  • Vue3とTypeScriptを組み合わせたプロジェクト開発の実践記録
  • Vue3とTypeScriptを組み合わせたプロジェクト開発の実践の概要

<<:  Dockerコンテナを更新、パッケージ化、Alibaba Cloudにアップロードする方法

>>:  一般的なSQL削除ステートメントの原則の違いを理解するだけです

推薦する

MySQL でデータベースを作成した後、ユーザー 'root'@'%' によるデータベース 'xxx' へのアクセスが拒否される問題を解決する

序文最近、仕事で問題が発生しました。データベースを作成した後、データベースに接続するときにエラーが発...

Kylin V10 への zabbix-agent のインストール手順

1. インストールパッケージをダウンロードするダウンロードアドレス: https://sourcef...

CSS導入方法4つのまとめ(共有)

1. インライン参照:ラベルに直接使用されるが、メンテナンスコストが高い スタイル='フォ...

JavaScriptコールバック関数の詳細な理解

目次序文クイックレビュー: JavaScript 関数関数とは何ですか?関数を宣言する関数の呼び出し...

MySQLデスクトップツールSQLyogのリソースとアクティベーション方法は、白黒のコマンドラインに別れを告げます

では、早速リソースについて見ていきましょう。 123WORDPRESS.COM ダウンロードSQLy...

MySQLデータベーステーブルの容量を確認する方法の例

この記事では、MySQL のデータベース テーブルの容量を確認するためのコマンド ステートメントを紹...

CentOS 8にdockerをインストールする最も詳細な方法

CentOS 8にDockerをインストールする公式ドキュメント: https://docs.doc...

Vue はグラフィック検証コードログインを実装します

この記事では、グラフィック認証コードログインを実装するためのVueの具体的なコードを参考までに紹介し...

Tomcatサーバーのセキュリティ設定方法

Tomcat は、Java Community Process を通じて Sun が開発した、広く使...

JavaScriptでシンプルなスクロールウィンドウを実装する

この記事では、スクロールウィンドウを実装するためのJavaScriptの具体的なコードを参考までに紹...

js を使ってシンプルな虫眼鏡効果を実現

この記事の例では、参考までに簡単な虫眼鏡効果を実現するためのjsの具体的なコードを共有しています。具...

Vue での mixin の応用について議論する

Mixin は、再利用可能な機能を Vue コンポーネント間で分散する非常に柔軟な方法を提供します。...

JS配列の次元削減のいくつかの方法の詳細な説明

2次元配列の次元削減配列インスタンスメソッド concat と ES6 スプレッド演算子を使用した次...

MySQL の暗黙的な型変換によって発生するインデックス障害の解決策

目次質問再生暗黙的な変換要約する参照する質問仕事中、1 つの SQL クエリ ステートメントのみを実...

学生情報管理システムを実装するためのJavaScript+HTML

目次1. はじめに2. レンダリング3. コード4. 学生情報管理システムのメインインターフェース1...