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削除ステートメントの原則の違いを理解するだけです

推薦する

HTML+CSS ボックスモデルの例 (円、半円など) 「border-radius」はシンプルで使いやすい

多くの友人は、フロントエンドを学習するときに、ボックス モデルがデフォルトで正方形であることに気付き...

フォームの「Enter」、「Submit」、「Enter != Submit」を削除する方法

「Enter != Submit」問題を実装するには、通常、「ボタンの種類」と「入力ボックスの数」か...

Founder フォント ライブラリの中国語と英語のファイル名比較表

Founder Type Library は、Founder Type Library ビジネス チ...

MySQL ストアド プロシージャの作成、呼び出し、管理の詳細な説明

目次ストアドプロシージャの概要ストアド プロシージャを使用する理由は何ですか?ストアドプロシージャの...

MySQLの認証コマンドgrantの使い方

この記事の例は MySQL 5.0 以降で実行されます。ユーザー権限を付与するための MySQL コ...

Linux で複数のファイルの名前を一括で変更する方法

Linux では、通常、ファイルの名前を変更するために mv コマンドを使用します。これは、単一のフ...

HTML タグのセマンティクス化 (H5 を含む)

導入HTML は、Web ドキュメントのコンテンツのコンテキスト構造と意味を提供します。HTML 自...

Vue シングルファイルコンポーネントの実装

最近、vue について読みました。これまで基本的に見落としていた単一ファイル コンポーネントを見つけ...

ブラウザの自動フォーム入力によるウェブページのスタイル損失の原因の分析と解決

バックエンドからフロントエンドまで、なんと悲劇なのでしょう。他の人の CSS を自分の jsp We...

JavaScriptアップロードファイル制限パラメータケースの詳細な説明

プロジェクトシナリオ: 1. アップロードファイルの制限関数: 1. フロントエンド操作による異常な...

ノードの対応するバージョンに関する簡単な説明 node-sass sass-loader

目次ノードのバージョンが一致しない、ノードをアップグレードまたはダウングレードするnvm を使用して...

一般的なCSS3アニメーションの実装方法

1. 何ですかCSS アニメーションは、CSS を使用して拡張マークアップ言語 (XML) 要素をア...

CSS プロパティ display:flow-root 宣言を 1 つの記事でマスターする

zhangxinxu より https://www.zhangxinxu.com/wordpress...

Linux で NFS のワンクリック展開を実装する方法

サーバー情報管理サーバー: m01 172.16.1.61サーバー: nfs01 172.16.1....

CentOS7 に ElasticSearch 6.4.1 をインストールするための詳細なチュートリアル

1. ElasticSearch 6.4.1 インストール パッケージを次の場所からダウンロードしま...