Vue2.0源碼閱讀筆記(五):模板編譯

  在使用Vue進行實際開發的過程當中,大多數時候使用模板來建立HTML,模板功能強大且簡潔直觀,最終模板會編譯成渲染函數,本文主要介紹模板編譯的具體過程。
html

1、編譯入口

  Vue從可否處理 template 選項的角度分爲兩個版本:運行時+編譯器只包含運行時運行時+編譯器版本也被稱爲完整版只包含運行時完整版體積小30%左右,使用只包含運行時版本須要藉助 vue-loadervueify 等工具編譯模板。
  本文從 web 平臺的編譯入口開始探究 Vue 完整版的模板編譯過程。在 src/platforms/web/entry-runtime-with-compiler.js 文件下的 $mount 方法中經過 compileToFunctions 方法將模板編譯成渲染函數。編譯方法的生成過程以下如所示:
前端

編譯入口
   首先,向 createCompilerCreator() 函數傳入 baseCompile() 函數,返回值爲 createCompiler() 函數。
  基礎編譯函數 baseCompile 代碼以下所示:

function baseCompile (template, options){
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}
複製代碼

  這幾行代碼是Vue模板編譯的核心,由以上代碼能夠看出,編譯的第一步是將模板經過 parse 函數解析成 AST(抽象語法樹),第二步優化AST,第三步根據優化後的抽象語法樹生成包含渲染函數字符串的對象。
  其次,向createCompiler() 函數傳入基本配置對象 baseOptions,返回包含函數屬性 compilecompileToFunctions 的對象。
  compile 函數接收兩個參數:模板字符串以及編譯選項。另外還經過閉包引用了前面傳入的基礎編譯函數 baseCompile 與基本編譯配置對象 baseOptions。該函數的功能主要有三點:
vue

一、合併基礎配置選項與傳入的編譯選項,生成 finalOptions。
二、收集編譯過程當中的錯誤。
三、調用基礎編譯函數 baseCompile。
node

  compileToFunctions 函數是將 compile 函數做爲參數傳入 createCompileToFunctionFn() 函數生成的返回值。createCompileToFunctionFn 函數定義一個緩存變量 cache,而後返回函數 compileToFunctions。模板字符串的編譯比較費時,使用緩存變量 cache 是爲了防止重複編譯,從而提高性能。
  compileToFunctions 函數接受三個參數:模板字符串、編譯選項、Vue實例。該函數的主要做用有如下五點:
web

一、緩存編譯結果,防止重複編譯。
二、檢測內容安全策略,保證 new Function() 可以使用。
三、調用 compile 函數將模板字符串轉成渲染函數字符串
四、調用 createFunction 函數將渲染函數字符串轉成真正的渲染函數
五、打印編譯錯誤。
正則表達式

  最後,將要編譯的模板字符串、編譯選項與 Vue 的實例對象傳入 compileToFunctions 函數,返回包含 renderstaticRenderFns 屬性的對象。
  render 爲最終生成的渲染函數字符串,staticRenderFns 爲存儲靜態根節點渲染函數字符串。這些函數字符串會經過 new Function() 來生成最終的渲染函數。
  Vue利用函數柯里化的技巧生成編譯模板的方法,在初讀代碼的時候讓人感受十分繁瑣,實際倒是設計的十分巧妙。
  這樣設計的緣由是 Vue 可以在不一樣平臺運行,好比在服務器端作SSR,也能夠在weex下使用。不一樣平臺都會有編譯過程,所依賴的基本編譯選項 baseOptions 會有所不一樣。Vue 將基礎的編譯過程抽離出來,而且能夠在多處添加編譯器選項,而後將添加的編譯器選項和基本編譯選項合併起來,最終靈活實如今不一樣平臺下的編譯。
express

2、生成AST

  關於 AST 的概念參照以下維基百科的描述:
編程

  在計算機科學中,抽象語法樹(Abstract Syntax Tree,AST),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。之因此說語法是「抽象」的,是由於這裏的語法並不會表示出真實語法中出現的每一個細節。
  在源代碼的翻譯和編譯過程當中,語法分析器建立出分析樹,而後從分析樹生成AST。一旦AST被建立出來,在後續的處理過程當中,好比語義分析階段,會添加一些信息。
數組

  Vue 編譯過程的核心的第一步是調用 parse 方法將模板字符串解析爲 AST 。
瀏覽器

const ast = parse(template.trim(), options)
複製代碼

  生成AST的過程分爲兩步:詞法分析句法分析parse 函數中實現的功能主要是句法分析詞法分析功能由 parse 內部調用的 parseHTML 函數來完成。咱們首先分析模板字符串作詞法分析的過程。

一、詞法分析函數 parseHTML

  parseHTML 函數的省略具體細節的代碼以下所示:

export function parseHTML (html, options) {
  const stack = []
  let last, lastTag
  /*省略。。。*/
  while (html) {
    last = html
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')

      if (textEnd === 0) {/*省略具體實現*/}

      let text, rest, next

      if (textEnd >= 0) {/*省略具體實現*/}
      if (textEnd < 0) { text = html }

      if (text) { advance(text.length) }

      if (options.chars && text) {
        options.chars(text, index - text.length, index)
      }
    } else {
      /*省略具體實現*/
    }

    if (html === last) {/*省略具體實現*/}
  }

  parseEndTag()

  function advance (n) {
    index += n
    html = html.substring(n)
  }

  function parseStartTag () {/*省略具體實現*/}

  function handleStartTag (match) {/*省略具體實現*/}

  function parseEndTag (tagName, start, end) {/*省略具體實現*/}
}
複製代碼

(一)、總體流程分析

  parseHTML 函數的具體功能以下圖所示:

parser函數
   parseHTML 逐個字符解析模板字符串。在 while 循環中,每次解析完一段字符串後都調用 advance 函數刪除已解析的字符串。
  在瞭解具體流程以前,先要弄明白一個問題: 如何判斷一個非一元標籤是否缺乏結束標籤呢?即如何檢測出像如下例子中發生錯誤的狀況:

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

  parseHTML 函數利用棧的數據結構來實現的:解析到開始標籤時,將開始標籤推入到數組 stack 中,變量 lastTag 始終指向棧頂元素。當解析到結束標籤時,會與棧頂的開始元素相匹配,若是是一對非一元標籤,則將棧頂開始標籤推出棧,同時繼續向前解析。若是匹配失敗或者解析完畢後棧中仍有開始標籤,則表示非一元標籤未閉合。
  如上例所示,先將 <div> 推入數組 stack 中,繼續解析後將 <span> 也推入棧中,此時棧頂標籤爲 <span>,解析到結束標籤 </div> 時會與棧頂標籤對比,<span></div> 不是一對非一元便籤,則說明模板字符串缺乏 <span> 的結束標籤。
  parseHTML 函數首先判斷將要解析的字符串是否是在純文本標籤裏的內容,純文本標籤是指 <script><style><textarea> ,若是爲純文本標籤的內容,則抽取純文本標籤裏的內容,直接使用傳入的 chars() 進行處理。
  若是不是在純文本標籤裏的內容,則根據字符 '<' 的位置來判斷要解析的字符串開頭是標籤仍是文本。若是是文本,則使用傳入的 chars() 進行處理。
  若是是標籤,則有五種可能性:

一、如果註釋標籤 <!---->,則使用傳入的 comment() 方法處理註釋內容。
二、如果條件註釋標籤<!--[]>,則不作任何處理,直接跳過。
三、如果文檔類型聲明<!DOCTYPE>,則不作任何處理,直接跳過。
四、如果結束標籤,則調用 parseEndTag() 函數處理。
五、如果開始標籤,則調用 parseStartTag()handleStartTag() 函數進行處理。

  總之,parseHTML 函數解析到文本調用 chars() 方法處理,解析到註釋標籤調用 comment() 方法處理,解析到條件註釋標籤文檔類型聲明跳過不作處理, chars()comment() 做爲傳入的方法將會在講解 parse() 方法時加以講解。
  對開始標籤與結束標籤的處理相對麻煩一些,在調用傳入的處理開始標籤與結束標籤的函數以前,parseHTML 函數會先對其作一些處理。

(二)、對開始標籤的處理

  解析開始標籤是會首先調用 parseStartTag() 函數,而後將函數返回值做爲參數傳入 handleStartTag() 函數進行處理。
  parseStartTag() 函數利用正則表達式來解析開始標籤,各項解析結果做爲 match 對象的屬性。

match = {
  tagName: '', // 開始標籤的標籤名
  attrs: [], // 標籤中各屬性的信息數組
  start: startIndex, // 標籤開始下標
  unarySlash: undefined || '/', // 判斷標籤是否爲一元標籤
  end: endIndex // 標籤結束下標
}
複製代碼

  handleStartTag() 函數接收 match 對象做爲參數。主要有如下五個功能:

一、stack 棧頂標籤爲 <p>,且當前解析的開始標籤爲段落式內容模型時,調用 parseEndTag() 方法閉合 <p>。
二、當前解析標籤能夠省略結束標籤,且與棧頂標籤相同,則調用 parseEndTag() 方法關閉當前解析標籤而後給出警告。
三、格式化 match.attrs 存儲屬性數組,格式化後 attrs 爲對象數組,每一個對象有兩個屬性:name(屬性名)、value(解碼後的屬性值)。
四、將當前解析標籤的信息推入到 stack 中,並將變量 lastTag 的值改爲棧頂標籤名稱。
五、調用傳入的 start 函數,參數爲當前解析標籤的信息。

(三)、對結束標籤的處理

  解析結束標籤是會調用 parseEndTag() 函數。該函數主要有如下四個功能:

一、檢測是否缺乏閉合標籤。
二、處理 stack 棧中剩餘的標籤。
三、處理 </br></p> 標籤。
四、調用傳入的 end() 方法處理結束標籤。

  在 handleStartTag() 函數中有講到遇到 <p> 調用 parseEndTag() 函數的狀況。如下是 <p> 標籤MDN的介紹:

起始標籤是必需的,結束標籤在如下情形中能夠省略。
<p>元素後緊跟<address>, <article>, <aside>, <blockquote>, 
<div>, <dl>, <fieldset>, <footer>, <form>, <h1>, <h2>, <h3>, 
<h4>, <h5>, <h6>, <header>, <hr>, <menu>, <nav>, <ol>, <pre>, 
<section>, <table>, <ul>或另外一個<p>元素;
或者父元素中沒有其餘內容了,並且父元素不是<a>元素。
複製代碼

  若是 <p> 後面跟以上元素,parseEndTag() 函數會模擬瀏覽器的行爲,自動補全 <p> 標籤。以下所示:

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

  上述html代碼會被解析成以下代碼:

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

  在 handleStartTag() 函數中講到:當前解析標籤能夠省略結束標籤,且與棧頂標籤相同,則調用 parseEndTag() 方法。 parseEndTag() 會閉合第二個標籤,並因第一個標籤未閉合而發出警告。

<li>123<li>456
複製代碼

  上述html代碼會被解析成以下代碼,並警告第一個標籤未閉合。

<li>123<li></li>456
複製代碼

  另外,僅僅寫下閉合標籤 </p></br> 時,瀏覽器會將 </p> 轉化成 <p></p>,將 </br> 轉化成 <br> 。Vue在轉換模板字符串的時候與瀏覽器保持一致,在 handleStartTag() 函數中將這兩個閉合標籤進行轉換處理。

二、句法分析函數 parse

  句法分析函數 parse 的代碼在 /src/compiler/parser/index.js 中。省略具體內容的 parse 函數代碼以下所示:

export function parse (template,options){
  const stack = []
  let root
  let currentParent
  /*省略。。。*/

  parseHTML(template, {
    // 省略一些參數
    start (tag, attrs, unary, start, end) {/*省略具體實現*/},
    end (tag, start, end) {/*省略具體實現*/},
    chars (text, start, end) {/*省略具體實現*/},
    comment (text, start, end) {/*省略具體實現*/}
  })
  return root
}
複製代碼

(一)、句法分析函數總體分析

  變量 rootparseHTML 函數的返回值,即最終生成的AST。Vue將模板中節點分爲四種:標籤節點包含字面量表達式的文本節點普通文本節點註釋節點,其中普通文本節點與註釋節點都是純文本節點,算做同一類型。
  AST中的節點描述對象有三種類型:標籤節點描述對象、表達式文本節點描述對象、純文本節點描述對象。不一樣類型節點描述對象的基本屬性以下所示:

// 標籤節點類型描述對象基本屬性
element = {
  type: 1, // 標籤節點類型標識
  tag: '', // 標籤名稱
  attrsList: [], // 對象數組,對象存儲着標籤屬性的名和值
  attrsMap: {}, // 標籤屬性對象,以鍵值對的形式存儲標籤屬性
  rawAttrsMap: {} // 將attrsList轉化爲對象,其屬性爲標籤屬性名
  parent: {}, // 父標籤節點
  children: [], // 子節點數組
  start: Number, // 開始標籤第一個字符在html字符串的位置
  end: Number // 結束標籤最後一個字符在html字符串的位置
}

// 表達式文本節點描述對象基本屬性
expression = {
  type: 2, // 表達式文本節點類型標識
  expression: '', // 表達式文本字符串,變量被 _s() 包裹
  tokens: [] // 存儲文本的token,有文本和表達式兩種類型
  text: '', // 文本字符串
  start: Number, // 表達式文本第一個字符在html字符串的位置
  end: Number // 表達式文本最後一個字符在html字符串的位置
}

// 純文本節點描述對象基本屬性
text = {
  type: 3, // 純文本節點類型標識
  text: '', // 文本字符串
  start: Number, // 純文本第一個字符在html字符串的位置
  end: Number // 純文本最後一個字符在html字符串的位置
}
複製代碼

  AST是樹狀結構的對象,經過標籤節點描述對象的 parentchildren 來實現。parent 屬性指向父節點元素描述對象,children 屬性存儲着該節點全部子節點的元素描述對象。根節點的 parent 屬性值爲 undefined
  變量 stackcurrentParent 配合使用來完成將子節點正確添加到父節點 children 屬性中的任務。stack 是棧的數據結構,用來存儲當前解析的節點的父節點以及祖先節點。currentParent 指向當前解析內容的父節點。
  在詞法分析的過程當中,解析節點時會調用對應的函數進行處理,下面分別加以介紹。

(二)、開始標籤處理函數 start

  在 start() 函數中,首先會調用 createASTElement() 函數,將標籤名、標籤屬性以及標籤的父節點做爲參數傳入,生成一個標籤節點類型描述對象。

let element = createASTElement(tag, attrs, currentParent)
複製代碼

  此時標籤節點對象以下所示:

element = {
  type: 1,
  tag,
  attrsList: attrs,
  attrsMap: makeAttrsMap(attrs),
  rawAttrsMap: {},
  parent,
  children: []
}
複製代碼

  若是開始標籤是 svg 或者 math,則額外添加 ns 屬性,屬性值與標籤名相同。接着向 element 對象添加 startend 屬性,使用 attrsList 屬性格式化 rawAttrsMap 屬性。
  而後調用 preTransforms 函數數組中的每個函數來處理 element 對象,以及以 process 開頭的一系列函數。在 parse 函數所在的文件中聲明瞭不少 process* 函數,好比 processForprocessIfprocessOnce等。這些函數和 preTransforms 函數數組中的函數做用都是同樣的,都是用來對當前元素描述對象作進一步處理。這是出於平臺化的考慮,將這一系列的函數放在不一樣的文件夾裏。process 系列函數是通用的,而 preTransforms 函數數組根據平臺不一樣而不一樣。
  這些根據不一樣屬性對 element 進行不一樣處理的過程至關繁雜,本文的主旨是講述模板字符串到渲染函數的編譯過程,這些具體的屬性處理會在後續文章講述相應指令時詳細闡述。
  最後,判斷開始標籤是否爲一元標籤,若是是則調用 closeElement 方法進行處理,closeElement 方法的具體內容將在下一節介紹;若是不是則將 element 對象賦值給變量 currentParent,做爲後續解析的父節點存在,並將 element 對象推入 stack 棧中。

(三)、結束標籤處理函數 end

  結束標籤處理函數 end 邏輯相對簡單,代碼以下所示:

end (tag, start, end) {
  const element = stack[stack.length - 1]
  // pop stack
  stack.length -= 1
  currentParent = stack[stack.length - 1]
  if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
    element.end = end
  }
  closeElement(element)
}
複製代碼

  首先將棧頂節點取出賦值給 element 變量,而後刪除 stack 中棧頂節點,並將 currentParent 變量指向棧頂節點。這樣作由於當前節點做爲父節點的狀況已經處理完畢,要將做用域還給上層節點。
  接着將 end 方法添加在結束標籤所在的節點上,最後將 element 變量傳入 closeElement 函數。
  closeElement 函數除了調用 postTransforms 數組中的函數處理節點以外,還根據不一樣狀況調用對應的 process* 對節點進行進一步處理。該函數的另外一主要功能是將當前節點推入到父節點 children 屬性中,並添加 parent 節點指向父節點。

currentParent.children.push(element)
element.parent = currentParent
複製代碼

(四)、文本處理函數 chars

  函數 chars 的核心代碼以下所示:

let res
let child
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].t!== ' ') {
  child = {
    type: 3,
    text
  }
}
if (child) {
  if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
    child.start = start
    child.end = end
  }
  children.push(child)
}
複製代碼

  函數 chars 會調用 parseText 函數處理文本字符串,parseText 主要解析包含字面量表達式的文本,若是文本中沒有字面量表達式則返回空值,不然返回包含 expressiontokens 屬性的對象。
  若文本包含字面量表達式,則生成 type 值爲2的節點描述對象,若爲純文本,則生成 type 值爲3的節點描述對象。而後將字符串開始字符的位置 start 與 結束字符的位置 end 添加到節點對象上,最後將節點描述對象推入到父節點的 children 數組屬性中。
  舉個例子,其中 title 爲變量數據:

<div>標題:{{title}}。</div>
<div>456<div>
複製代碼

  第一個<div> 標籤下的包含的包含字面量表達式的文本被 parseText 解析後返回以下對象:

{
  expression: "標題:"+_s(title)+"。",
  tokens: [ "標題:", { @binding: "title" }, "。" ]
}
複製代碼

  第一個<div> 標籤下文本最終生成的節點描述對象爲:

{
  type: 2,
  expression: "標題:"+_s(title)+"。",
  tokens: [ "標題:", { @binding: "title" }, "。" ],
  text: "標題:{{title}}。",
  start: Number,
  end: Number
}
複製代碼

  第二個<div> 標籤下文本最終生成的節點描述對象爲:

{
  type: 3,
  text: "456",
  start: Number,
  end: Number
}
複製代碼

(五)、註釋文本處理函數 comment

  註釋文本處理的邏輯跟 chars 函數中處理不含字面量表達式的文本很像,只是生成的 type 值爲3的節點描述對象多了一個屬性:isComment,其值爲 true,是註釋文本描述節點的標識。處理函數 comment 代碼以下所示:

comment (text, start, end) {
  if (currentParent) {
    const child = {
      type: 3,
      text,
      isComment: true
    }
    if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
      child.start = start
      child.end = end
    }
    currentParent.children.push(child)
  }
}
複製代碼

3、優化AST

  AST 的優化途徑主要是檢測出不須要更改的DOM的純靜態子樹,這樣作有兩個好處:

一、將純靜態節點描述對象提高爲常量,在從新渲染時不用從新生成。
二、在 Virtual DOM patching 的過程跳過這部分。

  AST的優化是經過 optimize 函數來完成的,函數代碼以下:

export function optimize (root, options) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  
  markStatic(root)
  
  markStaticRoots(root, false)
}
複製代碼

  AST優化邏輯相對比較簡單,分爲兩步:

一、使用 markStatic 函數標記靜態節點。
二、使用 markStaticRoots 方法標記靜態根節點。

一、標記靜態節點

  標記靜態節點函數 markStatic 代碼以下所示:

function markStatic (node) {
  node.static = isStatic(node)
  if (node.type === 1) {
    /* 省略一些代碼 */
    }
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if (!child.static) {
        node.static = false
      }
    }
    /* 省略處理 if else 等指令的狀況,具體講解指令時補充 */
  }
}
複製代碼

  markStatic 函數首先調用 isStatic 函數判斷是否爲靜態節點,在節點描述對象上添加布爾變量 static 標識是否爲靜態節點。
  若是是元素節點且有子節點則遞歸調用 markStatic 函數處理每一個子節點,若是子節點中有一個不是靜態節點的,該元素節點就不是靜態節點,即 static 屬性值爲 false
  判斷節點是否爲靜態的函數 markStatic 代碼以下:

function isStatic (node) {
  if (node.type === 2) { return false }
  if (node.type === 3) { return true }
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
  ))
}
複製代碼

  判斷節點是否爲靜態的規則有如下四條:

一、含有字面表達式的文本節點爲非靜態節點。
二、純文本節點爲靜態節點。
三、節點描述對象擁有 pre 屬性(即標籤有 v-pre 屬性)爲靜態節點。
四、若是一個標籤節點同時知足如下條件即爲靜態節點:沒有使用 v-if、v-for、沒有使用除 v-once 外的其它指令、非平臺保留的標籤、不是組件、不是帶有 v-fortemplate 標籤的直接子節點、節點的全部屬性的 key 都知足靜態 key

二、標記靜態根節點

  標記靜態根節點的函數 markStaticRoots 代碼以下所示:

function markStaticRoots (node, isInFor) {
  if (node.type === 1) {
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true
      return
    } else {
      node.staticRoot = false
    }
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    /* 省略處理 if else 等指令的狀況,具體講解指令時補充 */
  }
}
複製代碼

  屬性 staticRoot 是用來標記節點是否爲靜態根節點的,只有標籤節點纔有多是靜態根節點,判斷靜態根節點的標準爲同時知足一下三點:

一、節點 statictrue,即爲靜態節點。
二、標籤節點擁有子節點。
三、標籤節點不是隻擁有一個純文本節點。

  之因此要求標籤節點不是隻擁有一個純文本節點,是將一個這樣的節點標記爲靜態根節點收益比較小,最好是讓其老是保持新鮮。
  判斷當前節點是否爲靜態根節點以後,會遞歸調用 markStaticRoots 函數處理該節點的每個子節點。
  總之,通過AST優化函數 optimize 處理以後,每一個節點的描述對象上增長了布爾類型的屬性 static 用來標識是否爲靜態節點。type 屬性爲1的標籤節點描述對象上增長了布爾類型的屬性 staticRoot 用來標識是否爲靜態根節點。

4、生成渲染函數

  將優化後的 AST 轉化成渲染函數字符串是在 generate 函數中完成的,代碼以下所示:

export function generate (ast, options){
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
複製代碼

  generate 函數代碼看似簡單,其中包含的邏輯卻比較複雜,由於要對各類各樣的狀況進行處理。本文經過一個簡單的例子來大體闡述AST生成渲染函數字符串的過程,對示例以外的其它指令例如:v-forv-if 等存在時的狀況在後續的具體文章中再加以介紹。

<div id="app" class="home" @click="showTitle">
    <div class="title">標題:{{title}}。</div>
    <div class="content">
      <span>456</span>
    </div>
  </div>
複製代碼

  以上模板字符串通過 parse 函數解析成AST,而後通過 optimize 函數優化以後的AST以下所示:

ast = {
  tag: "div",
  type: 1,
  attrs: [{dynamic: undefined,end: 13,name: "id",start: 5,value: ""app""}],
  attrsList: [
    { end: 13,name: "id",start: 5,value: "app" },
    { end: 45,name: "@click",start: 27,value: "showTitle" }
  ],
  attrsMap: {id: "app", class: "home", @click: "showTitle"},
  end: 178,
  events: {click: {dynamic: false,end: 45,start: 27,value: "showTitle"}},
  hasBindings: true,
  parent: undefined,
  plain: false,
  rawAttrsMap: {
    @click: {end: 45,name: "@click",start: 27,value: "showTitle"},
    class: {end: 26,name: "class",start: 14,value: "home"},
    id: {end: 13,name: "id",start: 5,value: "app"}
  },
  start: 0,
  static: false,
  staticClass: ""home"",
  staticRoot: false,
  children: [
    {
      tag: "div",
      type: 1,
      attrsList: [],
      attrsMap: {class: "title"},
      children: [{
        text: "標題:{{title}}。",
        type: 2,
        end: 87,
        expression: ""標題:"+_s(title)+""",
        start: 74,
        static: false,
        tokens: (3) ["標題:", {@binding: "title"}, "。"]
      }],
      end: 93,
      parent: {/*對父節點描述對象的引入*/},
      plain: false,
      rawAttrsMap: {
        class: {end: 73,name: "class",start: 60,value: "title"}
      },
      start: 55,
      static: false,
      staticClass: ""title"",
      staticRoot: false
    },
    {
      text: " ",
      type: 3,
      end: 102,
      start: 93,
      static: true
    },
    {
      tag: "div",
      type: 1,
      attrsList: [],
      attrsMap: {class: "content"},
      children: [
        {
          tag: "span",
          type: 1,
          attrsList: [],
          attrsMap: {},
          children: [{text: "456",type: 3,end: 145,start: 142,static: true}],
          end: 152,
          parent: {/*對父節點描述對象的引入*/},
          plain: true,
          rawAttrsMap: {},
          start: 136,
          static: true 
        }
      ],
      end: 167,
      parent: {/*對父節點描述對象的引入*/},
      plain: false,
      rawAttrsMap: {class: {end: 122,name: "class",start: 107,value: "content"}},
      start: 102,
      static: true,
      staticClass: ""content"",
      staticInFor: false,
      staticRoot: true
    }
  ]
}
複製代碼

  idapp<div> 節點描述對象 children 屬性數組中有三個對象,這是由於其兩個 <div> 子節點中間有空格,算做一個純文本節點。
  generate 函數首先根據傳入的配置參數對象 options 實例化 CodegenState 對象。類 CodegenState 的代碼以下所示:

export class CodegenState {
  constructor (options) {
    this.options = options
    this.warn = options.warn || baseWarn
    this.transforms = pluckModuleFunction(options.modules, 'transformCode')
    this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
    this.directives = extend(extend({}, baseDirectives), options.directives)
    const isReservedTag = options.isReservedTag || no
    this.maybeComponent = (el) => !!el.component || !isReservedTag(el.tag)
    this.onceId = 0
    this.staticRenderFns = []
    this.pre = false
  }
}
複製代碼

  在這裏咱們重點關注該對象上的 dataGenFnsstaticRenderFns 屬性。staticRenderFns 屬性是一個數組,存儲着靜態根節點的渲染函數字符串,是 generate 函數的返回對象屬性之一。dataGenFns 數組中存儲着選項 modules 中的 genData 函數,分別處理標籤描述對象的class:class屬性、style:style屬性。

dataGenFns = [
  function genData (el) {
    var data = '';
    if (el.staticClass) {
      data += "staticClass:" + (el.staticClass) + ",";
    }
    if (el.classBinding) {
      data += "class:" + (el.classBinding) + ",";
    }
    return data
  },
  function genData(el) {
    var data = '';
    if (el.staticStyle) {
      data += "staticStyle:" + (el.staticStyle) + ",";
    }
    if (el.styleBinding) {
      data += "style:(" + (el.styleBinding) + "),";
    }
    return data
  }
]
複製代碼

  在生成 CodegenState 的實例化對象 state 以後,generate 函數將 AST 和 state 傳入 genElement 函數,最終生成渲染函數字符串。genElement 函數跟示例html有關的代碼以下:

export function genElement (el, state) {
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } 
  /* 省略一些判斷條件 */
  else {
    let code
    /* 省略爲標籤組件的狀況 */
    let data
    if (!el.plain || (el.pre && state.maybeComponent(el))) {
      data = genData(el, state)
    }

    const children = el.inlineTemplate ? null : genChildren(el, state, true)
    code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })`
    /* 省略一些代碼 */
    return code
  }
}
複製代碼

  根據示例生成的 ast 狀況,在genElement 函數中會首先調用:

data = genData(el, state)
複製代碼

  genData 函數主要是處理標籤中的屬性,將其轉化成字符串返回。在標籤擁有 class 或者 style 屬性時會循環調用前面講過的 state.dataGenFns 數組中的函數加以處理。當前標籤描述對象的屬性通過 genData 函數處理後 data 值爲:

"{staticClass:"home",attrs:{"id":"app"},on:{"click":showTitle}}"
複製代碼

  而後調用 genChildren 函數處理當前標籤描述對象 children 屬性數組中的對象,即處理其子節點描述對象。

const children = genChildren(el, state, true)
複製代碼

  genChildren 函數代碼以下所示:

function genChildren (el,state,checkSkip,altGenElement,altGenNode) {
  const children = el.children
  if (children.length) {
    const el = children[0]
    /* 省略一些代碼 */
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${ normalizationType ? `,${normalizationType}` : '' }`
  }
}
複製代碼

  函數的主要邏輯是使用 genNode 函數分別處理對象的 children 屬性中的各個節點描述對象。

function genNode (node, state) {
  if (node.type === 1) {
    return genElement(node, state)
  } else if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}
複製代碼

  genNode 函數根據節點類型的不一樣分別調用不一樣的函數進行處理,使用 genElement 函數處理標籤節點、使用 genComment 函數處理註釋節點、使用 genText 函數處理文本節點
  註釋節點的處理方式比較簡單,直接用 _e() 函數的字符串形式包裝註釋節點的 text 屬性。

function genComment(comment) {
  return `_e(${JSON.stringify(comment.text)})`
}
複製代碼

  文本節點的處理函數 genText 使用 _v() 函數的字符串形式包裝文本內容,純文本節點內容爲節點描述對象 text 屬性的值,含字面量表達式的文本內容爲節點描述對象 expression 屬性的值。

function genText (text) {
  return `_v(${text.type === 2 ? text.expression // no need for () because already wrapped in _s() : transformSpecialNewlines(JSON.stringify(text.text)) })`
}
複製代碼

  接着講 genElement 函數,在拿到子節點的函數字符串後,使用逗號拼接標籤名、標籤屬性字符串、子節點函數字符串,最後使用 _v() 函數的字符串形式加以包裝。使用 new Function 處理後變成以下代碼:

_c(tag,data,children)
複製代碼

  classcontent<div> 是靜態根節點,在 genElement 中會調用 genStatic 函數處理。

function genStatic (el, state) {
  /* 省略一些代碼 */
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  /* ··· */
  return `_m(${ state.staticRenderFns.length - 1 }${ el.staticInFor ? ',true' : '' })`
}
複製代碼

  處理後的函數字符串會被推入到 state.staticRenderFns 數組中,靜態根節點函數字符串以下:

"with(this){return _c('div',{staticClass:"content"},[_c('span',[_v("456")])])}"
複製代碼

  總之,函數 generate 的返回值爲:

{
  render: "with(this){return _c('div',{staticClass:"home",attrs:{"id":"app"},on:{"click":showTitle}},[_c('div',{staticClass:"title"},[_v("標題:"+_s(title)+"")]),_v(" "),_m(0)])}",
  staticRenderFns: ["with(this){return _c('div',{staticClass:"content"},[_c('span',[_v("456")])])}"]
}
複製代碼

  編譯實例代碼生成的函數字符串以及靜態根節點函數字符串通過 new Function 處理以後以下所示:

render = function() {
  with(this){
    return _c(
      'div',
      {
        staticClass:"home",
        attrs:{"id":"app"},
        on:{"click":showTitle}
      },
      [
        _c(
          'div',
          {staticClass:"title"},
          [_v("標題:"+_s(title)+"。")]
        ),
        _v(" "),
        _m(0)
      ]
    )
  }
}
複製代碼

  _c 函數定義在 src/core/instance/render.js 中,用來建立 VNode。其它的編譯渲染的內部函數定義在 src/core/instance/render-helpers/index.jsinstallRenderHelpers 函數中。
  _v 函數用來建立文本類型的 VNode_s 函數用來處理字面量表達式返回結果字符串;_m 函數處理靜態根節點。這些根據渲染函數生成 VNode 的過程會在後續講解 Virtual DOM 時詳細闡述。

5、總結

  Vue 使用函數柯里化的技巧來實現不一樣平臺下的編譯函數,核心編譯過程分爲三步:根據模板字符串生成AST、優化AST、根據AST生成渲染函數。
  生成AST的過程分爲兩步:詞法分析、語法分析。在詞法分析的過程當中,逐個字符的解析html字符串。首先判斷待解析的字符串開頭是元素標籤仍是文本,標籤又分爲:開始標籤、結束標籤、註釋標籤、文檔類型聲明標籤和條件註釋標籤,而後根據待解析字符串的類型作相應的處理。句法分析函數 parse 根據詞法解析的結果生成三種節點描述對象:標籤節點描述對象、字面量表達式文本節點描述對象、純文本節點描述對象。AST依靠標籤節點的指向父節點的 parent 屬性與包含子節點的 children 屬性構建樹狀結構。
  AST的優化分爲兩步:標記靜態節點、標記靜態根節點。優化的主要途徑是標記出不須要重複編譯且DOM不會發生改變的靜態根節點,在作相關處理時忽略掉該類節點。
  渲染函數字符串的生成主要是根據AST將各類節點拼接成包裹在不一樣函數中的字符串,最後經過new Function 將函數字符串轉化成真正的渲染函數。

歡迎關注公衆號:前端桃花源,互相交流學習!

相關文章
相關標籤/搜索