《Vue不看源碼懂原理》系列——Vue模板編譯

咱們在Vue中會使用一些變量,表達式,指令來填充模板,可是這些語法在HTML中是不存在的,那麼Vue是如何對這樣的模板進行編譯的呢?javascript

模板編譯

模板編譯的主要做用是將Vue模板編譯爲渲染函數,首先將模板解析成AST(抽象語法樹),而後使用AST生成渲染函數。html

首先咱們要知道Vue每次渲染,都會生成一份新的vNode與舊的vNode進行對比,在生成渲染函數以前還會遍歷一遍AST,爲全部的靜態節點作一個編輯,在從新渲染時,不會生成新得節點,而是直接克隆已存在的以前的靜態節點。java

因此整體過程是:將模板解析成AST=>遍歷AST標記靜態節點=>使用AST生成渲染函數正則表達式

在這裏插入圖片描述

模板解析成AST

在這一步驟中,須要通過解析器將模板解析AST,而後還須要通過優化器,遍歷AST找出靜態節點並標記。算法

解析器

在解析器內部還分紅了文本解析器,HTML解析器和過濾器解析器。express

其中核心部分是HTML解析器,做用是用來解析字符串模板。變量解析器用於解析帶有模板的文本變量,而不帶用變量的文本節點就是剛纔所說的靜態節點,不須要解析。過濾器解析器用來解析過濾器。解析結果AST是一種以節點爲結構的樹形結構的對象,一個對象表示一個節點,對象的屬性用來保存節點所須要的數據。數組

解析模板例如:函數

<div>
  <p>{{name}}</p>
</div>
複製代碼

解析成AST以後:post

//裏面的內容後續會解釋
{
  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)"
      }]
    }
  ]
}
複製代碼

解析器在解析HTML的過程當中會不斷觸發各類鉤子函數。這些鉤子函數包括開始標籤鉤子函數、結束標籤鉤子函數、文本鉤子函數以及註釋鉤子函數。性能

例如:

parseHTML(template, {
    start (tag, attrs, unary) {
        // 每當解析到標籤的開始位置時,觸發該函數
    },
    end () {
        // 每當解析到標籤的結束位置時,觸發該函數
    },
    chars (text) {
        // 每當解析到文本時,觸發該函數
    },
    comment (text) {
        // 每當解析到註釋時,觸發該函數
    }
})
複製代碼

咱們簡單舉一個例子來講明上述方法是如何構建AST節點的:

<div><p>我是一個節點</p></div>
複製代碼

首先,解析器會將html模板做爲一段字符串模板從前向後進行解析,解析到<div>時,會觸發一個標籤開始的鉤子函數start();而後解析到<p>時,又觸發一次鉤子函數start();接着解析到我是一個節點這行文本,此時觸發了文本鉤子函數chars();而後解析到</p>,觸發了標籤結束的鉤子函數end();接着繼續解析到</div>,此時又觸發一次標籤結束的鉤子函數end(),解析結束。

start()函數你能夠看做爲HTML解析函數,他的三個參數分別是分別是tag、attrs和unary,分別表明標籤名、標籤的屬性以及是不是自閉合標籤。

而文本節點的解析函數chars和註釋節點的解析函數comment都只有一個參數text。這是由於構建元素節點須要知道標籤名、屬性和是不是自閉合元素,而構建註釋節點和文本節點時只須要知道文本內容便可。 咱們將上面的parseHTML()擴充一下:

//咱們模擬一個建立AST元素類型節點的函數
function createASTElement (tag, attrs, parent) {
    // 返回的是一個節點對象
    return {
        type: 1, // 指定節點類型 1.元素節點
        tag, // 指定節點
        attrsList: attrs, // 指定節點屬性
        parent, // 指定是不是自閉合標籤
        children: []
    }
}
parseHTML(template, {
    start (tag, attrs, unary) {
        // 每當解析到標籤的開始位置時,觸發該函數
        // 將標籤名、標籤的屬性以及是不是自閉合標籤傳入
        let element = createASTElement(tag, attrs, currentParent)
    },
    end () {
        // 每當解析到標籤的結束位置時,觸發該函數
    },
    chars (text) {
        // 每當解析到文本時,觸發該函數 
        // 返回的是一個文本節點對象 
        // 文本分兩種類型 2.帶變量的動態文本節點 3.不帶變量的純文本節點
        let element = {type: 3, text}
    },
    comment (text) {
        // 每當解析到註釋時,觸發該函數
        // 返回的是一個註釋節點對象,註釋文本和文本的區別是打上了isComment標記
        let element = {type: 3, text, isComment: true}
    }
})
複製代碼

可是使用上述方式建立的節點雖然帶有節點對象信息,可是是扁平的,沒有層級關係,而Vue使用了出入棧的方式來構建一個AST結構對象,爲以前的扁平數據實現層級關係。

每次解析HTML,都會使用一個棧來存儲維護,當觸發start()函數時,將當前構建的節點推入棧中;每當觸發鉤子函數end()時,就從棧中彈出上一個節點。舉個例子:

<div>
    <h1>我是h1</h1>
    <p>我是文本</p>
</div>
複製代碼
  1. 模板的開始位置是div的開始標籤,此時發現棧是空的,這說明div節點是根節點,由於它沒有父節點。最後,將div節點推入棧中,並將模板字符串中的div開始標籤從模板中截取掉
    在這裏插入圖片描述
  2. 鉤子函數裏會忽略空格,同時會在模板中將這些空格截取掉。接下來發現是h1的開始標籤,因而會觸發鉤子函數start,會先構建一個h1節點。此時發現棧裏存的最近一個節點是div節點,這說明h1節點的父節點是div,因而將h1添加到div的子節點中(也就是children中),而且將h1節點推入棧中,同時從模板中將h1的開始標籤截取掉。
    \[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-PphkP0lG-1585107481902)(https://s1.ax1x.com/2020/03/25/8XUvYn.jpg)\]
  3. 這時模板的開始位置是一段文本,因而會觸發鉤子函數chars。先構建一個文本節點,此時發現棧中的最後一個節點是h1,這說明文本節點的父節點是h1,因而將文本節點添加到h1節點的子節點中。因爲文本節點沒有子節點,因此文本節點不會被推入棧中。最後,將文本從模板中截取掉。
    在這裏插入圖片描述
  4. 這時模板的開始位置是h1結束標籤,因而會觸發鉤子函數end。end觸發後,會把棧中最後一個節點(也就是h1)彈出來。
    在這裏插入圖片描述
  5. 第2個標籤是p標籤和h1標籤同理,會先構建一個p節點,因爲第4步已經從棧中彈出了一個節點h1,因此此時棧中的最近一個節點是div,因而將p推入div的子節點中,最後將p推入到棧中,從模板中截取掉。而後會同樣構建文本節點,截取,最後根據p結束標籤觸發鉤子函數end,把p節點彈出來。
    在這裏插入圖片描述
  6. 最後開始位置是div的結束標籤,因而會觸發鉤子函數end。其邏輯與以前同樣,把棧中的最後一個節點div彈出來,並將div的結束標籤從模板中截取掉。HTML解析器已經運行完畢,這時咱們會發現棧已經空了,而咱們獲得了一個完整的帶層級關係的AST語法樹
    \[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qOVcHowI-1585107971471)(https://s1.ax1x.com/2020/03/25/8Xw1JA.jpg)\]

其中對開始標籤,結束標籤,還有標籤屬性的解析基本是使用了大量正則表達式:去解析<div </div> : class=這樣的字符串,去斷定這是一個什麼標籤該去觸發什麼函數,不作過多描述。

這個過程如何解析HTML中的註釋,條件註釋,DOCTYPE,文本?

HTML中的註釋,判斷<!--,經過indexOf找到註釋結束位置-->的下標,而後將結束位置前的字符都截取掉。條件註釋註釋用提早的表達式判斷<,條件註釋會被直接截取掉。DOCTYPE直接匹配這段字符,根據它的length屬性來決定要截取多長的字符串。文本咱們只須要找到>與下一個<在什麼位置,這以前的全部字符都屬於文本。

節點不完整?

<div><p></div>
複製代碼

在上面的代碼中,p標籤沒有結束標籤,那麼當HTML解析器解析到div的結束標籤時,發現棧內元素倒是p標籤。就會從棧頂向棧底遍歷尋找到div標籤,在找到div標籤以前遇到的全部其餘標籤都會標記爲忘記閉合的標籤,在非生產環境下在控制檯打印警告提示。

文本解析器

爲何文本解析器要單獨說,由於文本其實分兩種類型,一種是純文本,另外一種是帶變量的文本。

Hello name
	Hello {{name}}
複製代碼

若是是純文本,不須要進行任何處理;但若是是帶變量的文本,那麼須要使用文本解析器進一步解析。由於帶變量的文本在使用虛擬DOM進行渲染時,須要將變量替換成變量中的值。

  1. 第一步要作的事情就是使用正則表達式來判斷文本是不是帶變量的文本,也就是檢查文本中是否包含{{xxx}}這樣的語法。
  2. 咱們建立一個數組,把變量左邊的文本添加到數組中,而後把變量改爲_s(變量名)這樣的函數形式也添加到數組中。若是變量後面還有變量,則重複以上動做。
  3. 數組元素的順序和文本的順序是一致的,此時將這些數組元素用+連起來變成字符串(_s(變量名)是Vue中對應的解析變量函數,會返回該變量的值)

優化器

靜態節點:

<p>我就是一個純文本的靜態節點</p>
複製代碼

優化器則是將解析完的AST進行遍歷,找出靜態節點並標記,在下次更新對比虛擬DOM的vNode時,若是發現這兩個節點是靜態節點,則直接跳過更新節點的流程。達到進一步避免一些無用的DOM操做來提高性能,由於靜態節點在首次渲染後必定不會改變。

AST生成渲染函數

代碼生成器

代碼生成器是將解析完的AST轉化爲渲染函數須要的內容,這個內容叫代碼字符串,例如:

<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生成代碼字符串
`with(this) {return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])}`
複製代碼

以後將這串代碼字符串傳到Vue的渲染函數中,渲染函數根據參數結構,調用相關的建立vNode的方法(生成後的代碼字符串中看到了有幾個函數調用 _c,_v,_s,這是Vue內部的一些渲染函數,_c能夠建立元素類型的vNode,_v能夠建立文本類型的vNode,_e能夠建立註釋類型的vNode)最後組成一份虛擬DOM結構。

咱們拿_c來解釋一下這個字符串的結構:

在這裏插入圖片描述
將其分解來看,拿建立元素類型的函數_c()來講,圖中1和3是第一個參數:HTML標籤名,圖中2和4是第三個參數:children,這個函數存在第二個可選項參數:元素上使用的屬性所對應的數據對象,例如:

<p title="biaoti">name</p>
複製代碼
with(this){
  return _c(
    'p', // 標籤名
    {
      attrs:{"title":"biaoti"},
    }, // 屬性
    [_v("name")] // 子節點
  )
}
複製代碼

代碼生成器的整體邏輯其實就是遞歸ATS,而後根據ATS結構拼出這樣的_c('div',[_c('p',[_v(_s(name))])]) 字符串,再將其傳入渲染函數執行。

至於具體的AST轉換過程就不作深刻解釋,會令文章顯得枯燥。

總結

咱們以上簡單講述了Vue對模板編譯的總體流程:解析器(模板字符串轉換成AST),優化器(標記靜態節點)和代碼生成器(將AST裝換成帶結構的代碼字符串)。

解析器經過使用一個棧來維護節點,每從模板字符串中截取一個節點字符串,就將其推入棧中,同時構建一個AST節點,一直到結束節點在將其推出棧,如此循環最後構建出一套帶有結構的AST對象。

優化器是經過遍歷AST節點,對其中的靜態節點作標記,同時最後標記處根靜態節點,節省部分沒必要要的性能消耗。

代碼生成器也是經過遍歷去拼出一個渲染函數執行的代碼字符串,遍歷的過程根據不一樣的節點類型type調用不一樣的生成字符串方法,最後拼出一個完整的 render 函數須要的代碼字符串。

後續還有兩篇:

《Vue不看源碼懂原理》系列——Vue的diff算法不難懂(直接傳送)

《Vue不看源碼懂原理》系列——Vue的實例函數和指令解密(下週)

之有一篇用心總結的《Javascript垃圾回收原理》沒太有響應,我以爲你們能夠看一看,耐心一下的話比較好理解。

點個贊,我加油

點關注,不迷路,哈哈哈

相關文章
相關標籤/搜索