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

推薦する

Linux 編集の開始、停止、再起動の Springboot jar パッケージ スクリプトの例

序文springboot設定ファイルでは、設定ファイルの名前には独自の意味と用途があります。 dev...

ナビゲーションバーコンポーネントをVueでカプセル化する

はじめに:ナビゲーション バーなどのコンポーネント ベースのアイデアを使用して機能モジュールを完全に...

CSS設定div背景画像実装コード

コンポーネントに背景画像コントロールを追加するには、次の 2 つの手順だけが必要です。 <表示...

Webデザイナーの成長体験

<br />まず最初に、私はこのグループの中では完全な新人だということを述べなければなり...

nginx 設定ファイルパスとリソースファイルパスを表示する方法

nginx 設定ファイルのパスを表示する nginx -t 経由nginx -t コマンドの本来の機...

vsftpd ユーザーが ssh 経由でログインすることを禁止する方法

序文vsftp は使いやすく安全な FTP サーバー ソフトウェアです。システムユーザーまたは仮想ユ...

Nginx をインストールして複数のドメイン名を設定する方法

Nginx のインストールCentOS 6.x yum にはデフォルトで nginx ソフトウェア ...

Nginx でアクセス頻度、ダウンロード速度、同時接続数を制限する方法

1. アクセス頻度、同時接続、ダウンロード速度を制限するために使用されるモジュールと命令の概要ngx...

MySQL 5.7.21 解凍版インストール Navicat データベース操作ツールインストール

MySQL解凍版とNavicatデータベース操作ツールのインストールは、以下のとおりです。 1. M...

React と Threejs を使用して VR パノラマ プロジェクトを作成する詳細なプロセス

最近、 Three.jsでReactを使用して、720 度のパノラマ写真を閲覧できるプロジェクトを構...

インスピレーションを得るための7つのクールなダイナミックウェブサイトデザイン

デザインの分野では、毎年さまざまなデザインのトレンドや流行があります。たとえば、近年のレスポンシブデ...

JavaScriptにおける評価戦略の詳細な説明

目次それを覆う栗パラメータの受け渡し値渡し共同配送要約する拡張機能 - 遅延評価私は最近、JavaS...

JavaScript の手ぶれ補正とスロットリングの説明

目次安定スロットリング要約する安定自動ドアは人を感知してドアを開け、5 秒間のカウントダウンを開始し...

Linux カーネル デバイス ドライバー カーネル時間管理に関する注意事項

/****************** * Linux カーネルの時間管理 ***********...

MySQL のマスタースレーブレプリケーションと読み取り書き込み分離の原理と使用法の詳細な説明

この記事では、例を使用して、MySQL マスター/スレーブ レプリケーションと読み取り/書き込み分離...