[Vue源碼]一起來學Vue模板編譯原理(一)-Template生成AST

来源:https://www.cnblogs.com/yzsunlei/archive/2019/12/30/12118330.html
-Advertisement-
Play Games

本文我們一起通過學習Vue模板編譯原理(一) Template生成AST來分析Vue源碼。預計接下來會圍繞Vue源碼來整理一些文章,如下。 "一起來學Vue雙向綁定原理 數據劫持和發佈訂閱" "一起來學Vue模板編譯原理(一) Template生成AST" "一起來學Vue模板編譯原理(二) AST ...


本文我們一起通過學習Vue模板編譯原理(一)-Template生成AST來分析Vue源碼。預計接下來會圍繞Vue源碼來整理一些文章,如下。

這些文章統一放在我的git倉庫:https://github.com/yzsunlei/javascript-series-code-analyzing。覺得有用記得star收藏。

編譯過程

模板編譯是Vue中比較核心的一部分。關於Vue編譯原理這塊的整體邏輯主要分三個部分,也可以說是分三步,前後關係如下:

第一步:將模板字元串轉換成element ASTs(解析器)

第二步:對 AST 進行靜態節點標記,主要用來做虛擬DOM的渲染優化(優化器)

第三步:使用element ASTs生成render函數代碼字元串(代碼生成器)

對應的Vue源碼如下,源碼位置在src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 1.parse,模板字元串 轉換成 抽象語法樹(AST)
  const ast = parse(template.trim(), options)
  // 2.optimize,對 AST 進行靜態節點標記
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 3.generate,抽象語法樹(AST) 生成 render函數代碼字元串
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

這篇文檔主要講第一步將模板字元串轉換成對象語法樹(element ASTs),對應的源碼實現我們通常稱之為解析器。

解析器運行過程

在分析解析器的原理前,我們先舉例看下解析器的具體作用。

來一個最簡單的實例:

<div>
  <p>{{name}}</p>
</div>

上面的代碼是一個比較簡單的模板,它轉換成AST後的樣子如下:

{
  tag: "div"
  type: 1,
  staticRoot: false,
  static: false,
  plain: true,
  parent: undefined,
  attrsList: [],
  attrsMap: {},
  children: [
    {
      tag: "p"
      type: 1,
      staticRoot: false,
      static: false,
      plain: true,
      parent: {tag: "div", ...},
      attrsList: [],
      attrsMap: {},
      children: [{
        type: 2,
        text: "{{name}}",
        static: false,
        expression: "_s(name)"
      }]
    }
  ]
}

其實AST並不是什麼很神奇的東西,不要被它的名字嚇倒。它只是用JS中的對象來描述一個節點,一個對象代表一個節點,對象中的屬性用來保存節點所需的各種數據。

事實上,解析器內部也分了好幾個子解析器,比如HTML解析器、文本解析器以及過濾器解析器,其中最主要的是HTML解析器。顧名思義,HTML解析器的作用是解析HTML,它在解析HTML的過程中會不斷觸發各種鉤子函數。這些鉤子函數包括開始標簽鉤子函數、結束標簽鉤子函數、文本鉤子函數以及註釋鉤子函數。

我們先看下解析器整體的代碼結構,源碼位置src/compiler/parser/index.js

parseHTML(template, {
  warn,
  expectHTML: options.expectHTML,
  isUnaryTag: options.isUnaryTag,
  canBeLeftOpenTag: options.canBeLeftOpenTag,
  shouldDecodeNewlines: options.shouldDecodeNewlines,
  shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
  shouldKeepComment: options.comments,
  outputSourceRange: options.outputSourceRange,
  // 每當解析到標簽的開始位置時,觸發該函數
  start (tag, attrs, unary, start, end) {
    //...
  },
  // 每當解析到標簽的結束位置時,觸發該函數
  end (tag, start, end) {
    //...
  },
  // 每當解析到文本時,觸發該函數
  chars (text: string, start: number, end: number) {
    //...
  },
  // 每當解析到註釋時,觸發該函數
  comment (text: string, start, end) {
    //...
  }
})

實際上,模板解析的過程就是不斷調用鉤子函數的處理過程。整個過程,讀取template字元串,使用不同的正則表達式,匹配到不同的內容,然後觸發對應不同的鉤子函數處理匹配到的截取片段,比如開始標簽正則匹配到開始標簽,觸發start鉤子函數,鉤子函數處理匹配到的開始標簽片段,生成一個標簽節點添加到抽象語法樹上。

還舉上面那個例子來說:

<div>
  <p>{{name}}</p>
</div>

整個解析運行過程就是:解析到時,會觸發一個標簽開始的鉤子函數start,處理匹配片段,生成一個標簽節點添加到AST上;然後解析到

時,又觸發一次鉤子函數start,處理匹配片段,又生成一個標簽節點並作為上一個節點的子節點添加到AST上;接著解析到{{name}}這行文本,此時觸發了文本鉤子函數chars,處理匹配片段,生成一個帶變數文本(變數文本下麵會講到)標簽節點並作為上一個節點的子節點添加到AST上;然後解析到

,觸發了標簽結束的鉤子函數end;接著繼續解析到,此時又觸發一次標簽結束的鉤子函數end,解析結束。

正則匹配

模板解析過程會涉及到許許多多的正則匹配,知道每個正則有什麼用途,會更加方便之後的分析。

那我們先來看看這些正則表達式,源碼位置在src/compiler/parser/index.js

export const onRE = /^@|^v-on:/
export const dirRE = process.env.VBIND_PROP_SHORTHAND
  ? /^v-|^@|^:|^\.|^#/
  : /^v-|^@|^:|^#/
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
const dynamicArgRE = /^\[.*\]$/

const argRE = /:(.*)$/
export const bindRE = /^:|^\.|^v-bind:/
const propBindRE = /^\./
const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g

const slotRE = /^v-slot(:|$)|^#/

const lineBreakRE = /[\r\n]/
const whitespaceRE = /\s+/g

const invalidAttributeRE = /[\s"'<>\/=]/

上面這些正則相對來說比較簡單,基本上都是用來匹配Vue中自定義的一些語法格式,如onRE匹配 @ 或 v-on 開頭的屬性,forAliasRE匹配v-for中的屬性值,比如item in items、(item, index) of items。

下麵這些就是專門針對html的一些正則匹配,源碼位置在src/compiler/parser/html-parser.js

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/

這些正則表達式相對來說就複雜一些,如attribute用來匹配標簽的屬性,startTagOpen、startTagClose用於匹配標簽的開始、結束部分等。這些正則表達式的寫法就不多說了,有興趣的朋友可以針對這些正則一個一個的去測試一下。

HTML解析器

這裡我們來看看HTMl解析器。

事實上,解析HTML模板的過程就是迴圈的過程,簡單來說就是用HTML模板字元串來迴圈,每輪迴圈都從HTML模板中截取一小段字元串,然後重覆以上過程,直到HTML模板被截成一個空字元串時結束迴圈,解析完畢。

我們通過源碼,就可以看到整個函數邏輯就是被一個while迴圈包裹著。源碼位置在:src/compiler/parser/html-parser.js

export function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  let index = 0
  let last, lastTag
  while (html) {
    //...
  }
  
  parseEndTag()
  
  //...
}

下麵我用一個簡單的模板,模擬一下HTML解析的過程,以便於更好的理解。

<div>
  <p>{{text}}</p>
</div>

最初的HTML模板:

<div>
  <p>{{text}}</p>
</div>

第一輪迴圈時,截取出一段字元串,解析出是div開始標簽並且觸發鉤子函數start,截取後的結果為:


  <p>{{text}}</p>
</div>

第二輪迴圈時,截取出一段換行空字元串,會觸發鉤子函數chars,截取後的結果為:

  <p>{{text}}</p>
</div>

第三輪迴圈時,截取出一段字元串

,解析出是p開始標簽並且觸發鉤子函數start,截取後的結果為:

  {{text}}</p>
</div>

第四輪迴圈時,截取出一段字元串{{name}},解析出是變數字元串並且觸發鉤子函數chars,截取後的結果為:

  </p>
</div>

第五輪迴圈時,截取出一段字元串

,解析出是p閉合標簽並且觸發鉤子函數end,截取後的結果為:


</div>

第六輪迴圈時,截取出一段換行空字元串,會觸發鉤子函數chars,截取後的結果為:

</div>

第七輪迴圈時,截取出一段字元串,解析出是div閉合標簽並且觸發鉤子函數end,截取後的結果為:

第八輪迴圈時,發現只有一個空字元串,解析完畢,迴圈結束。

現在,是不是就對HTML解析過程很清楚了。其實迴圈過程對每次匹配到的片段進行分析記錄還是很複雜的,因為被截取的片段分很多種類型,比如:

開始標簽,例如<div>

結束標簽,例如</div>

HTML註釋,例如<!-- 註釋 -->

DOCTYPE,例如<!DOCTYPE html>

條件註釋,例如<!--[if !IE]>-->註釋<!--<![endif]-->

文本,例如'字元串'

對每個片段的具體處理這裡就不說了,有興趣的直接看源碼去。

文本解析器

文本解析器是對HTML解析器解析出來的文本進行二次加工。文本其實分兩種類型,一種是純文本,另一種是帶變數的文本。如下:

這種就是純文本:

這裡有段文本

這種就是帶變數的文本:

文本內容:{{text}}

上面HTML解析器在解析文本時,並不會區分文本是否是帶變數的文本。如果是純文本,不需要進行任何處理;但如果是帶變數的文本,那麼需要使用文本解析器進一步解析。因為帶變數的文本在使用虛擬DOM進行渲染時,需要將變數替換成變數中的值。

我們知道,HTML解析器在碰到文本時,會觸發chars鉤子函數,我們先來看看鉤子函數裡面是怎麼區分普通文本和變數文本的。

源碼位置在:src/compiler/parser/html-parser.js

chars (text: string, start: number, end: number) {
  //...
  let child: ?ASTNode
  if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
    child = {
      type: 2,
      expression: res.expression,
      tokens: res.tokens,
      text
    }
  } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
    child = {
      type: 3,
      text
    }
  }
  //...
  children.push(child)
}

我們重點看res = parseText(text,delimiters)這一行,通過條件判斷設置不同的類型。事實上type=2表示表達式類型,type=3表示普通文本類型。

我們再來看看parseText函數具體做了什麼

export function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  // 匹配不到帶變數時直接返回了
  if (!tagRE.test(text)) {
    return
  }
  const tokens = []
  const rawTokens = []
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  // 對匹配到的變數迴圈處理成表達式
  while ((match = tagRE.exec(text))) {
    index = match.index
    // push text token
    // 先把 { { 前邊的文本添加到tokens中
    if (index > lastIndex) {
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // tag token
    const exp = parseFilters(match[1].trim())
    // 使用_s對變數進行包裝
    // 把變數改成`_s(x)`這樣的形式也添加到數組中
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    // 設置lastIndex來保證下一輪迴圈時,正則表達式不再重覆匹配已經解析過的文本
    lastIndex = index + match[0].length
  }
  // 當所有變數都處理完畢後,如果最後一個變數右邊還有文本,就將文本添加到數組中
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

實際上這個函數就是處理帶變數的文本,首先如果是純文本,直接return。如果是帶變數的文本,使用正則表達式匹配出文本中的變數,先把變數左邊的文本添加到數組中,然後把變數改成_s(x)這樣的形式也添加到數組中。如果變數後面還有變數,則重覆以上動作,直到所有變數都添加到數組中。如果最後一個變數的後面有文本,就將它添加到數組中。

那麼對於上面示例處理結果如下:

parseText('這裡有段文本')
// undefined

parseText('文本內容:{{text}}')
// '"文本內容:" + _s(text)'

好了,對於文本解析器就這麼多內容。

總結一下

模板解析是Vue模板編譯的第一步,即通過模板得到AST(抽象語法樹)。

生成AST的過程核心就是藉助HTML解析器,當HTML解析器通過正則匹配到不同的片段時會觸發對應不同的鉤子函數,通過鉤子函數對匹配片段進行解析我們可以構建出不同的節點。

文本解析器是對HTML解析器解析出來的文本進行二次加工,主要是為了處理帶變數的文本。

相關


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 會動的漢克狗: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Cartoon Dog</title> </head> <body> <div class="dog"> <div class="head"> ...
  • Css3文本與字體 文本陰影 h1 { text-shadow: 5px 5px 5px red; } word-break換行: h1:nth-child(1) { word-break: normal; } /*英文:一行放不下時整個單詞換行*/ h1:nth-child(2) { word-b ...
  • HTTP狀態碼的英文為 HTTP Status Code。下麵是常見的HTTP狀態碼: 200 - 請求成功 301 - 資源(網頁等)被永久轉移到其它URL 404 - 請求的資源(網頁等)不存在 500 - 內部伺服器錯誤 1、HTTP狀態碼的分類 HTTP狀態碼由三個十進位數字組成,第一個十進 ...
  • 如需代碼,詳情請咨詢郵箱,謝謝配合 ...
  • Css3選擇器相關: section > div直接子元素選擇器 div + article相鄰兄弟選擇器(在元素之後出現) div ~ article通用兄弟選擇器(在元素之後出現) 屬性選擇器: a[href] { text-decoration: none; } a[href="#"] { c ...
  • $("#divSetting").on("click", function () { $(this).toggleClass("open"); }); ...
  • 遇到一個小問題,記錄一下 問題:在微信小程式中使用scroll-view標簽時,用height:cale(xx - xx)設置高度無效,在page中設置高度為百分百依舊無效 解決辦法:直接在最大的view標簽中設置高度為百分百即可 ...
  • 有一段時間沒有更新技術博文了,因為這段時間埋下頭來看Vue源碼了。本文我們一起通過學習雙向綁定原理來分析Vue源碼。預計接下來會圍繞Vue源碼來整理一些文章,如下。 "一起來學Vue雙向綁定原理 數據劫持和發佈訂閱" "一起來學Vue模板編譯原理(一) Template生成AST" "一起來學Vue ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...