在使用Vue進行實際開發的過程當中,大多數時候使用模板來建立HTML,模板功能強大且簡潔直觀,最終模板會編譯成渲染函數,本文主要介紹模板編譯的具體過程。
html
Vue從可否處理 template 選項的角度分爲兩個版本:運行時+編譯器、只包含運行時。運行時+編譯器版本也被稱爲完整版。只包含運行時比完整版體積小30%左右,使用只包含運行時版本須要藉助 vue-loader 或 vueify 等工具編譯模板。
本文從 web 平臺的編譯入口開始探究 Vue 完整版的模板編譯過程。在 src/platforms/web/entry-runtime-with-compiler.js 文件下的 $mount 方法中經過 compileToFunctions 方法將模板編譯成渲染函數。編譯方法的生成過程以下如所示:
前端
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,返回包含函數屬性 compile 與 compileToFunctions 的對象。
compile 函數接收兩個參數:模板字符串以及編譯選項。另外還經過閉包引用了前面傳入的基礎編譯函數 baseCompile 與基本編譯配置對象 baseOptions。該函數的功能主要有三點:
vue
一、合併基礎配置選項與傳入的編譯選項,生成 finalOptions。
二、收集編譯過程當中的錯誤。
三、調用基礎編譯函數 baseCompile。
node
compileToFunctions 函數是將 compile 函數做爲參數傳入 createCompileToFunctionFn() 函數生成的返回值。createCompileToFunctionFn 函數定義一個緩存變量 cache,而後返回函數 compileToFunctions。模板字符串的編譯比較費時,使用緩存變量 cache 是爲了防止重複編譯,從而提高性能。
compileToFunctions 函數接受三個參數:模板字符串、編譯選項、Vue實例。該函數的主要做用有如下五點:
web
一、緩存編譯結果,防止重複編譯。
二、檢測內容安全策略,保證 new Function() 可以使用。
三、調用 compile 函數將模板字符串轉成渲染函數字符串
四、調用 createFunction 函數將渲染函數字符串轉成真正的渲染函數
五、打印編譯錯誤。
正則表達式
最後,將要編譯的模板字符串、編譯選項與 Vue 的實例對象傳入 compileToFunctions 函數,返回包含 render 與 staticRenderFns 屬性的對象。
render 爲最終生成的渲染函數字符串,staticRenderFns 爲存儲靜態根節點渲染函數字符串。這些函數字符串會經過 new Function() 來生成最終的渲染函數。
Vue利用函數柯里化的技巧生成編譯模板的方法,在初讀代碼的時候讓人感受十分繁瑣,實際倒是設計的十分巧妙。
這樣設計的緣由是 Vue 可以在不一樣平臺運行,好比在服務器端作SSR,也能夠在weex下使用。不一樣平臺都會有編譯過程,所依賴的基本編譯選項 baseOptions 會有所不一樣。Vue 將基礎的編譯過程抽離出來,而且能夠在多處添加編譯器選項,而後將添加的編譯器選項和基本編譯選項合併起來,最終靈活實如今不一樣平臺下的編譯。
express
關於 AST 的概念參照以下維基百科的描述:
編程
在計算機科學中,抽象語法樹(Abstract Syntax Tree,AST),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。之因此說語法是「抽象」的,是由於這裏的語法並不會表示出真實語法中出現的每一個細節。
在源代碼的翻譯和編譯過程當中,語法分析器建立出分析樹,而後從分析樹生成AST。一旦AST被建立出來,在後續的處理過程當中,好比語義分析階段,會添加一些信息。
數組
Vue 編譯過程的核心的第一步是調用 parse 方法將模板字符串解析爲 AST 。
瀏覽器
const ast = parse(template.trim(), options)
複製代碼
生成AST的過程分爲兩步:詞法分析、句法分析。parse 函數中實現的功能主要是句法分析,詞法分析功能由 parse 內部調用的 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 函數的具體功能以下圖所示:
<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 的代碼在 /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
}
複製代碼
變量 root 爲 parseHTML 函數的返回值,即最終生成的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是樹狀結構的對象,經過標籤節點描述對象的 parent 與 children 來實現。parent 屬性指向父節點元素描述對象,children 屬性存儲着該節點全部子節點的元素描述對象。根節點的 parent 屬性值爲 undefined 。
變量 stack 與 currentParent 配合使用來完成將子節點正確添加到父節點 children 屬性中的任務。stack 是棧的數據結構,用來存儲當前解析的節點的父節點以及祖先節點。currentParent 指向當前解析內容的父節點。
在詞法分析的過程當中,解析節點時會調用對應的函數進行處理,下面分別加以介紹。
在 start() 函數中,首先會調用 createASTElement() 函數,將標籤名、標籤屬性以及標籤的父節點做爲參數傳入,生成一個標籤節點類型描述對象。
let element = createASTElement(tag, attrs, currentParent)
複製代碼
此時標籤節點對象以下所示:
element = {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
複製代碼
若是開始標籤是 svg 或者 math,則額外添加 ns 屬性,屬性值與標籤名相同。接着向 element 對象添加 start、end 屬性,使用 attrsList 屬性格式化 rawAttrsMap 屬性。
而後調用 preTransforms 函數數組中的每個函數來處理 element 對象,以及以 process 開頭的一系列函數。在 parse 函數所在的文件中聲明瞭不少 process* 函數,好比 processFor、processIf、processOnce等。這些函數和 preTransforms 函數數組中的函數做用都是同樣的,都是用來對當前元素描述對象作進一步處理。這是出於平臺化的考慮,將這一系列的函數放在不一樣的文件夾裏。process 系列函數是通用的,而 preTransforms 函數數組根據平臺不一樣而不一樣。
這些根據不一樣屬性對 element 進行不一樣處理的過程至關繁雜,本文的主旨是講述模板字符串到渲染函數的編譯過程,這些具體的屬性處理會在後續文章講述相應指令時詳細闡述。
最後,判斷開始標籤是否爲一元標籤,若是是則調用 closeElement 方法進行處理,closeElement 方法的具體內容將在下一節介紹;若是不是則將 element 對象賦值給變量 currentParent,做爲後續解析的父節點存在,並將 element 對象推入 stack 棧中。
結束標籤處理函數 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 的核心代碼以下所示:
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 主要解析包含字面量表達式的文本,若是文本中沒有字面量表達式則返回空值,不然返回包含 expression 與 tokens 屬性的對象。
若文本包含字面量表達式,則生成 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
}
複製代碼
註釋文本處理的邏輯跟 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)
}
}
複製代碼
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-for 的 template 標籤的直接子節點、節點的全部屬性的 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 是用來標記節點是否爲靜態根節點的,只有標籤節點纔有多是靜態根節點,判斷靜態根節點的標準爲同時知足一下三點:
一、節點 static 爲 true,即爲靜態節點。
二、標籤節點擁有子節點。
三、標籤節點不是隻擁有一個純文本節點。
之因此要求標籤節點不是隻擁有一個純文本節點,是將一個這樣的節點標記爲靜態根節點收益比較小,最好是讓其老是保持新鮮。
判斷當前節點是否爲靜態根節點以後,會遞歸調用 markStaticRoots 函數處理該節點的每個子節點。
總之,通過AST優化函數 optimize 處理以後,每一個節點的描述對象上增長了布爾類型的屬性 static 用來標識是否爲靜態節點。type 屬性爲1的標籤節點描述對象上增長了布爾類型的屬性 staticRoot 用來標識是否爲靜態根節點。
將優化後的 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-for、v-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
}
]
}
複製代碼
id 爲 app 的 <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
}
}
複製代碼
在這裏咱們重點關注該對象上的 dataGenFns 與 staticRenderFns 屬性。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)
複製代碼
class 爲 content 的 <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.js 的 installRenderHelpers 函數中。
_v 函數用來建立文本類型的 VNode;_s 函數用來處理字面量表達式返回結果字符串;_m 函數處理靜態根節點。這些根據渲染函數生成 VNode 的過程會在後續講解 Virtual DOM 時詳細闡述。
Vue 使用函數柯里化的技巧來實現不一樣平臺下的編譯函數,核心編譯過程分爲三步:根據模板字符串生成AST、優化AST、根據AST生成渲染函數。
生成AST的過程分爲兩步:詞法分析、語法分析。在詞法分析的過程當中,逐個字符的解析html字符串。首先判斷待解析的字符串開頭是元素標籤仍是文本,標籤又分爲:開始標籤、結束標籤、註釋標籤、文檔類型聲明標籤和條件註釋標籤,而後根據待解析字符串的類型作相應的處理。句法分析函數 parse 根據詞法解析的結果生成三種節點描述對象:標籤節點描述對象、字面量表達式文本節點描述對象、純文本節點描述對象。AST依靠標籤節點的指向父節點的 parent 屬性與包含子節點的 children 屬性構建樹狀結構。
AST的優化分爲兩步:標記靜態節點、標記靜態根節點。優化的主要途徑是標記出不須要重複編譯且DOM不會發生改變的靜態根節點,在作相關處理時忽略掉該類節點。
渲染函數字符串的生成主要是根據AST將各類節點拼接成包裹在不一樣函數中的字符串,最後經過new Function 將函數字符串轉化成真正的渲染函數。
歡迎關注公衆號:前端桃花源,互相交流學習!