知道嗎?Vue.js 有 2 個(gè)版本,一個(gè)是Runtime + Compiler版本,另一個(gè)是Runtime only版本。Runtime + Compiler版本是包含編譯代碼的,簡單來說就是Runtime only版本不包含編譯代碼的,在運(yùn)行時(shí)候,需要借助 webpack 的 vue-loader 事先把模板編譯成 render 函數(shù)。
假如在你需要在客戶端編譯模板 (比如傳入一個(gè)字符串給 template 選項(xiàng),或掛載到一個(gè)元素上并以其 DOM 內(nèi)部的 HTML 作為模板),就將需要加上編譯器,即完整版:
// 需要編譯器 new Vue({ template: '<div>{{ hi }}</div>' }) // 不需要編譯器 new Vue({ render (h) { return h('div', this.hi) } })
當(dāng)使用 vue-loader 或 vueify 的時(shí)候,*.vue 文件內(nèi)部的模板會(huì)在構(gòu)建時(shí)預(yù)編譯成 JavaScript。其實(shí)要知道并不需要編譯器的,只用繼續(xù)運(yùn)行就可以。因?yàn)檫\(yùn)行時(shí)版本相比完整版體積要小大約 30%,所以應(yīng)該盡可能使用這個(gè)版本。
在 Vue 的整個(gè)編譯過程中,會(huì)做三件事:
解析模板parse,生成 AST
優(yōu)化 ASToptimize
生成代碼generate
對(duì)編譯過程的了解會(huì)讓我們對(duì) Vue 的指令、內(nèi)置組件等有更好的理解。不過由于編譯的過程是一個(gè)相對(duì)復(fù)雜的過程,我們只要求理解整體的流程、輸入和輸出即可,對(duì)于細(xì)節(jié)我們不必?fù)柑?xì)。由于篇幅較長,這里會(huì)用三篇文章來講這三件事。這是第一篇, 模板解析,template -> AST
注:全文源碼來源,Vue(2.6.11),Runtime + Compiler 的 Vue.js
編譯準(zhǔn)備
這里先做一個(gè)準(zhǔn)備工作,編譯之前有一個(gè)嵌套的函數(shù)調(diào)用,看似非常的復(fù)雜,但是卻有玄機(jī)。有什么玄機(jī)?接著往下看。
源碼編譯鏈?zhǔn)秸{(diào)用
compileToFunctions
在源碼走了一遭,發(fā)現(xiàn)經(jīng)過一系列的調(diào)用,最后createCompiler函數(shù)返回的compileToFunctions函數(shù) 對(duì)應(yīng)的就是$mount函數(shù)調(diào)用的compileToFunctions方法,它是調(diào)用createCompileToFunctionFn方法的返回值。
// 偽代碼 function createCompilerCreator (baseCompile) { return function createCompiler (baseOptions) { function compile ( template, options ) { ... return compiled } return { compile: compile, compileToFunctions: createCompileToFunctionFn(compile) } } } function createCompileToFunctionFn (compile) { var cache = Object.create(null); return function compileToFunctions ( template, options, vm ) { ... } }
方法接受三個(gè)參數(shù)。
編譯模板 template
編譯配置 options
Vue 的實(shí)例
這個(gè)方法編譯的核心代碼就一行。
// compile var compiled = compile(template, options);
而 compile 方法的核心代碼也就一行。
const compiled = baseCompile(template, finalOptions)
并且baseCompile方法是在執(zhí)行createCompilerCreator方法執(zhí)行的時(shí)候傳入的。
var createCompiler = createCompilerCreator(function baseCompile ( template, options ) { var ast = parse(template.trim(), options); if (options.optimize !== false) { optimize(ast, options); } var code = generate(ast, options); return { ast: ast, render: code.render, staticRenderFns: code.staticRenderFns } });
baseCompile會(huì)做三件事情。
現(xiàn)在我們回到createCompilerCreator傳入的函數(shù)。
這是為什么?就是因?yàn)閂ue 本身是支持多平臺(tái)的編譯,在不同平臺(tái)下的編譯會(huì)有所有不同,但是在同一平臺(tái)編譯是相同的,所以在使用createCompiler(baseOptions)時(shí),baseOptions 會(huì)有所有不同。
在 Vue 中利用函數(shù)柯里化的思想,將baseOptions的配置參數(shù)進(jìn)行了保存。并且在調(diào)用鏈中,不斷的進(jìn)行函數(shù)調(diào)用并返回函數(shù)。
這其實(shí)也是利用了函數(shù)柯里化的思想把很多基礎(chǔ)的函數(shù)抽離出來, 通過 createCompilerCreator(baseCompile) 的方式把真正編譯的過程和其它邏輯如對(duì)編譯配置處理、緩存處理等剝離開,這樣的設(shè)計(jì)還是非常巧妙的。
編譯準(zhǔn)備已經(jīng)做完,我們接下來看看 Vue 是如何做parse的。
parse
parse要做的事情就是對(duì) template 做解析,生成 AST 抽象語法樹。
抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。
例如現(xiàn)在有這樣一段代碼:
<body> <div id="app"></div> <script> new Vue({ el: '#app', template: ` <ul> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> </ul> ` }); </script> </body>
經(jīng)過parse,就變成了一個(gè)嵌套的樹狀結(jié)構(gòu)的對(duì)象。
在 AST 中,每一個(gè)樹節(jié)點(diǎn)都是一個(gè) element,并且維護(hù)了上下文關(guān)系(父子關(guān)系)。
解析 template
parse的過程核心就是parseHTML函數(shù),這個(gè)函數(shù)的作用就是解析 template 模板。下面將解析過程中一些重要的點(diǎn)進(jìn)行一個(gè)抽象解讀。
function parseHTML (html, options) { var stack = []; ... // 遍歷模板字符串 while (html) { ... } // 清除所有剩余的標(biāo)簽 parseEndTag(); // 將 html 字符串的指針前移 function advance (n) { ... } // 解析開始標(biāo)簽 function parseStartTag () { ... } // 處理解析的開始標(biāo)簽的結(jié)果 function handleStartTag (match) { ... } // 解析結(jié)束標(biāo)簽 function parseEndTag (tagName, start, end) { ... } }
標(biāo)簽匹配相關(guān)的正則
接下來就是關(guān)于指令匹配相關(guān)的正則。相信很多人都有測試過。
// 識(shí)別合法的xml標(biāo)簽 var ncname = '[a-zA-Z_][\w\-\.]*'; // 復(fù)用拼接,這在我們項(xiàng)目中完成可以學(xué)起來 var qnameCapture = "((?:" + ncname + "\:)?" + ncname + ")"; // 匹配注釋 var comment =/^<!--/; // 匹配<!DOCTYPE> 聲明標(biāo)簽 var doctype = /^<!DOCTYPE [^>]+>/i; // 匹配條件注釋 var conditionalComment =/^<![/; // 匹配開始標(biāo)簽 var startTagOpen = new RegExp(("^<" + qnameCapture)); // 匹配解說標(biāo)簽 var endTag = new RegExp(("^<\/" + qnameCapture + "[^>]*>")); // 匹配單標(biāo)簽 var startTagClose = /^\s*(/?)>/; // 匹配屬性,例如 id、class var attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配動(dòng)態(tài)屬性,例如 v-if、v-else var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)[[^=]+][^\s"'<>/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
stack
變量stack,它定義一個(gè)棧,作用是存儲(chǔ)開始標(biāo)簽。例如我有一個(gè)這樣的簡單模板:
<div> <ul> <li>1</li> </ul> </div>
當(dāng)在 while 循環(huán)時(shí),如果遇到一個(gè)非單標(biāo)簽,就會(huì)將開始標(biāo)簽 push 到數(shù)組中,遇到閉合標(biāo)簽就開始元素出棧,這樣可以檢測我們寫的 template 是否符合嵌套、開閉規(guī)范,這也是檢測 html 字符串中是否缺少閉合標(biāo)簽的原理。
advance
advance函數(shù)貫穿這個(gè) template 的解析流程。當(dāng)我們在解析 template 字符串的時(shí)候,需要對(duì)字符串逐一掃描,直到結(jié)束。advance 函數(shù)的作用就是移動(dòng)指針。例如匹配<字符,指針移動(dòng) 1,匹配到<!--字符指針移動(dòng) 4。在整個(gè)解析過程中,貫穿著指針的移動(dòng),因?yàn)橐虢馕鐾瓿删捅仨毎涯0迦烤幾g完。
function advance (n) { index += n; html = html.substring(n); }
while
template 的 while 循環(huán)是解析中最重要的一環(huán),也是這一小節(jié)的重點(diǎn)。
循環(huán)的終止條件是 html 字符串為空,即 html 字符串全部編譯完畢。
循環(huán)時(shí),第一個(gè)判斷是判斷內(nèi)容是否在存純文本標(biāo)簽中。判斷的作用是: 確保我們沒有像腳本/樣式這樣的純文本內(nèi)容元素。
當(dāng)內(nèi)容不在純文本標(biāo)簽,判斷 template 字符串的第一個(gè)<字符位置,來進(jìn)行不同的操作。
var textEnd = html.indexOf('<');
當(dāng)前 template 第一個(gè)字符是 <
在這種場景下, template 會(huì)出現(xiàn)以下幾種情況,重點(diǎn)是解析開始標(biāo)簽和結(jié)束標(biāo)簽。
<!--開頭的注釋:會(huì)找到注釋的結(jié)尾,將注釋截取出來,移動(dòng)指針,并將注釋當(dāng)做當(dāng)前父節(jié)點(diǎn)的一個(gè)子元素存儲(chǔ)到 children 中。
<![開頭的 條件注釋:如果是條件注釋,會(huì)直接移動(dòng)指針,不做任何其他操作。
<!DOCTYPE開頭的 doctype:如果是 doctype,會(huì)直接移動(dòng)指針,不做任何其他操作。
<開頭的開始標(biāo)簽
<開頭的結(jié)束標(biāo)簽 接下來重點(diǎn)講講如何解析開始標(biāo)簽和結(jié)束標(biāo)簽。
解析開始標(biāo)簽
①,通過正則匹配到開始標(biāo)簽,如果匹配到就會(huì)返回一個(gè) match 的匹配結(jié)果。例如:
<div id="test-id" class="test-calss" v-show='show'></div>
template 中有一個(gè) div,當(dāng)匹配到開始標(biāo)簽(結(jié)束標(biāo)簽類似)時(shí),會(huì)返回這樣數(shù)組結(jié)果。
0: "<div"
1: "div"
groups: undefined
index: 0
input: "<div>\n <ul>\n <li>1</li>\n </ul>\n </div>"
length: 2
②,接下來: 定義了 match 變量,它是一個(gè)對(duì)象,初始狀態(tài)下?lián)碛腥齻€(gè)屬性:
tagName:存儲(chǔ)標(biāo)簽的名稱。div。
attrs :用來存儲(chǔ)將來被匹配到的屬性,例如:id、class、v-if 這些屬性。
start:初始值為 index,是當(dāng)前字符流讀入位置在整個(gè) html 字符串中的相對(duì)位置。 ③,然后通過advance函數(shù)移動(dòng)指針。
④,如果沒有匹配到開始標(biāo)簽的結(jié)束部分,并且存在屬性,就會(huì)遍歷找出所有屬性和動(dòng)態(tài)屬性。保存在 match 的 attrs 中。
⑤,上一步獲取了標(biāo)簽的屬性和動(dòng)態(tài)屬性,但是即使這樣并不能說明這是一個(gè)完整的標(biāo)簽,只有當(dāng)匹配到開始標(biāo)記的結(jié)束標(biāo)記時(shí),才能證明這是一個(gè)完整的標(biāo)簽,所以才會(huì)有這一步的判斷。varstartTagClose= /^\s*(/?)>/;并且標(biāo)記unarySlash屬性。
⑥,假設(shè)正常匹配了,有匹配結(jié)果,也返回了 match (結(jié)構(gòu)如上),就會(huì)走到handleStartTag這個(gè)函數(shù)的作用就是用來處理開始標(biāo)簽的解析結(jié)果,所以它接收 parseStartTag 函數(shù)的返回值作為參數(shù)。
handleStartTag 的核心邏輯很簡單,先判斷開始標(biāo)簽是否是一元標(biāo)簽,類似<img />、<br/>這樣,接著對(duì) match.attrs 遍歷并做了一些處理,最后判斷如果非一元標(biāo)簽,則往 stack 里 push 一個(gè)對(duì)象,并且把 tagName 賦值給 lastTag。
function parseStartTag () { // ① var start = html.match(startTagOpen); if (start) { // ② var match = { tagName: start[1], attrs: [], start: index }; // ③ advance(start[0].length); var end, attr; // ④ while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) { ... } // ⑤ if (end) { match.unarySlash = end[1]; advance(end[0].length); match.end = index; return match } } } // Start tag: var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); ... } // ⑥ function handleStartTag (match) { ... }
解析結(jié)束標(biāo)簽
有解析開始標(biāo)簽就會(huì)解析結(jié)束標(biāo)簽。所以接下來我們來看看如何解析結(jié)束標(biāo)簽。
①,正則匹配結(jié)束標(biāo)簽(具體的正則看前面)。
②,匹配到結(jié)束標(biāo)簽,進(jìn)行解析處理,獲取到結(jié)束標(biāo)簽的標(biāo)簽名稱、開始位置和結(jié)束位置,開始進(jìn)行解析操作。
③,查找同一類型的最近打開的標(biāo)記,并記錄位置。
④,如果存在同一類型的標(biāo)記,就將 stack 中匹配的標(biāo)記彈出。
⑤,如果沒有同一類型的標(biāo)記,分別處理</br>、</p>標(biāo)簽。這是為了和瀏覽器保持同樣的行為。舉個(gè)例子:在代碼中,分別寫了</br>、</p>的結(jié)束標(biāo)簽,但注意我們并沒有寫起始標(biāo)簽,但是瀏覽器是能夠正常解析他們的,其中</br>標(biāo)簽被正常解析為<br>標(biāo)簽,而</p>標(biāo)簽被正常解析為<p></p>。除了br與p其他任何標(biāo)簽如果你只寫了結(jié)束標(biāo)簽?zāi)敲礊g覽器都將會(huì)忽略。所以為了與瀏覽器的行為相同,parseEndTag 函數(shù)也需要專門處理br與p的結(jié)束標(biāo)簽,即:</br> 和</p>。
<div> </br> </p> </div>
// ① var endTagMatch = html.match(endTag); if (endTagMatch) { var curIndex = index; advance(endTagMatch[0].length); // ② // 獲取到結(jié)束標(biāo)簽的標(biāo)簽名稱、開始位置和結(jié)束位置 parseEndTag(endTagMatch[1], curIndex, index); continue } function parseEndTag (tagName, start, end) { ... // ③ if (tagName) { lowerCasedTagName = tagName.toLowerCase(); for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { pos = 0; } // ④ if (pos >= 0) { ... stack.length = pos; lastTag = pos && stack[pos - 1].tag; // ⑤ } else if (lowerCasedTagName === 'br') { if (options.start) { options.start(tagName, [], true, start, end); } } else if (lowerCasedTagName === 'p') { if (options.start) { options.start(tagName, [], false, start, end); } if (options.end) { options.end(tagName, start, end); } } }
到這里結(jié)束標(biāo)簽頁解析完成,但是在 Vue 中對(duì)開始標(biāo)簽和結(jié)束標(biāo)簽的解析遠(yuǎn)不止這樣,因?yàn)闉榱藶g覽器行為保持一下在解析的過程中還會(huì)對(duì)一些特殊標(biāo)簽特殊處理,典型的就是p、br標(biāo)簽,我會(huì)在后面出一篇文章來詳細(xì)講講 Vue 是如何處理它們的。
當(dāng)前 template 不存在 <
當(dāng)解析到的 template 中不存在 < 時(shí),這認(rèn)為是一個(gè)文本。操作很簡單就是移動(dòng)指針。
并且這里在源碼中發(fā)現(xiàn)初始化變量的時(shí)候,都是這樣寫的 var text =(void0), rest =(void0), next =(void0);而不是直接 var xx = undefined,這樣做就是為了更加安全。
JavaScript void 運(yùn)算符
if (textEnd < 0) { text = html; } if (text) { advance(text.length); }
當(dāng)前 template < 不在第一個(gè)字符串
這里的判斷處理就是為了處理我們在一些純文本中也會(huì)寫<標(biāo)記的場景。例如:
<div>1<2</div>
現(xiàn)在有這樣一段模塊,<div>被解析之后,還剩1<2,這時(shí)解析到存在<標(biāo)記但是位置不在第一個(gè)。就循環(huán)找出包含<的這一段文本,并將這一段當(dāng)成一個(gè)純文本處理。
if (textEnd >= 0) { rest = html.slice(textEnd); while ( !endTag.test(rest) && !startTagOpen.test(rest) && !comment.test(rest) && !conditionalComment.test(rest) ) { next = rest.indexOf('<', 1); if (next < 0) { break } textEnd += next; rest = html.slice(textEnd); } text = html.substring(0, textEnd); }
處理 stack 棧中剩余未處理的標(biāo)簽
當(dāng) while 循環(huán)解析了一遍 template 之后,會(huì)再調(diào)用一次parseEndTag,這樣做的目的是為了處理 stack 棧中剩余未處理的標(biāo)簽。當(dāng)調(diào)用時(shí),沒有傳遞任何參數(shù),也意味著tagName, start, end都是空的,這時(shí) pos 為 0 ,所以 i >= pos 始終成立,這個(gè)時(shí)候 stack 棧中如果有剩余未處理的標(biāo)簽,則會(huì)逐個(gè)警告缺少閉合標(biāo)簽,并調(diào)用 options.end 將其閉合。
// Clean up any remaining tags parseEndTag(); function parseEndTag (tagName, start, end) { if (tagName) { ... } else { pos = 0; } if (pos >= 0) { for (var i = stack.length - 1; i >= pos; i--) { if (i > pos || !tagName && options.warn) { options.warn( ("tag <" + (stack[i].tag) + "> has no matching end tag."), { start: stack[i].start, end: stack[i].end } ); } } ... } }
到這里解析 template 的重點(diǎn)過程都基本結(jié)束了,整個(gè)過程就是遍歷 template 字符串,然后通過正則一點(diǎn)一點(diǎn)的匹配解析字符串,直到整個(gè)字符串被解析完成。
生成 AST
當(dāng)然解析完 template 目的是生成 AST,經(jīng)過上面的一些列操作,只是解析完 template 字符串,并沒有生成一顆 AST 抽象語法樹。正常的來說抽象語法樹應(yīng)該是如下這樣的,節(jié)點(diǎn)與節(jié)點(diǎn)之間通過 parent 和 children 建立聯(lián)系,每個(gè)節(jié)點(diǎn)的 type 屬性用來標(biāo)識(shí)該節(jié)點(diǎn)的類別,比如 type 為 1 代表該節(jié)點(diǎn)為元素節(jié)點(diǎn),type 為 3 代表該節(jié)點(diǎn)為文本節(jié)點(diǎn)。
生成 AST 的主要步驟是在解析的過程中,會(huì)調(diào)用對(duì)應(yīng)的鉤子函數(shù)。解析到開始標(biāo)簽,就調(diào)用開始的鉤子函數(shù),解析到結(jié)束標(biāo)簽就調(diào)用結(jié)束的鉤子函數(shù),解析到文本就會(huì)調(diào)用文本的鉤子,解析到注釋就調(diào)用注釋的鉤子函數(shù)。這些鉤子函數(shù)就會(huì)將所有的節(jié)點(diǎn)串聯(lián)起來,并生成 AST 樹的結(jié)構(gòu)。
start 鉤子函數(shù)
這個(gè)鉤子函數(shù)會(huì)在解析到開始標(biāo)簽的時(shí)候被調(diào)用。為了更加清楚解析過程,我們引入如下一個(gè)模板,如下:
<div><span></span><p></p></div>
解析 <div>
①,解析到<div>會(huì)調(diào)用 start 鉤子函數(shù)。
②,創(chuàng)建一個(gè)基礎(chǔ)元素對(duì)象。
{ type: 1, tag:"div", parent: null, children: [], attrsList: [] } function createASTElement ( tag, attrs, parent ) { return { type: 1, tag: tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, parent: parent, children: [] } }
③,接著判斷 root 是否存在,如果不存在則直接將 element 賦值給 root 。root 是一個(gè)記錄值,也就是最后解析返回的整個(gè) AST。
④,如果當(dāng)前標(biāo)簽不是一元標(biāo)簽時(shí),會(huì)將當(dāng)前的element賦值給currentParent目的是為建立父子元素的關(guān)系。
⑤,將元素入棧,入棧的目的是為了做回退操作,這里先不講為什么需要做回退,后面在講。此時(shí)stack = [{ tag : "div"... }]。
// parseHTML函數(shù) 解析到開始標(biāo)簽 function handleStartTag (match) { if (options.start) { // ① options.start(tagName, attrs, unary, match.start, match.end); } } // start 鉤子函數(shù) start: { // ② var element = createASTElement(tag, attrs, currentParent); // element: // { // type: 1, // tag:"div", // parent: null, // children: [], // attrsList: [] // } // ③ if (!root) { root = element; } // ④ if (!unary) { currentParent = element; // currentParent: // { // type: 1, // tag:"div", // parent: null, // children: [], // attrsList: [] // } // ⑤ stack.push(element); } }
解析<span>
接著解析到<span>。此時(shí) root 已經(jīng)存在,currentParent 也存在,所以會(huì)將 span 元素的描述對(duì)象添加到 currentParent 的 children 數(shù)組中作為子節(jié)點(diǎn),并將自己的 parent 元素進(jìn)行標(biāo)記。所以最終生成的描述對(duì)象為:
{ type: 1, tag:"div", parent: {/*div 元素的描述*/}, attrsList: [] children: [{ type: 1, tag:"span", parent: div, attrsList: [], children:[] }], }
此時(shí) stack = [{ tag : "div"... }, {tag : "span"...}]。
end 鉤子函數(shù)
當(dāng)解析到結(jié)束標(biāo)簽就會(huì)調(diào)用結(jié)束標(biāo)簽的鉤子函數(shù),還是這段模板代碼,解析完<div><span>后遇到了</span>。
1
<div><span></span><p></p></div>
解析
①,首先就是保存最后一個(gè)元素,將 stack 的最后一個(gè)元素刪除,也就是變成 stack = [{tag: "div" ...}],這就是做了一個(gè)回退操作 。
②,設(shè)置 currentParent 為 stack 的最后一個(gè)元素。
end: function end (tag, start, end$1) { // ① var element = stack[stack.length - 1]; stack.length -= 1; // ② currentParet = stack[stack.length - 1]; ... },
為什么回退?
解析 <p>
當(dāng)再次解析到開始標(biāo)簽時(shí),就會(huì)再次調(diào)用 start 鉤子函數(shù),這里重點(diǎn)是在解析 p 的開始標(biāo)簽時(shí):stack = [{tag:"div"...},{tag:"p"...}] ,由于在解析到上一個(gè)</span>標(biāo)簽時(shí)做了一個(gè)回退操作, 這就能保證在解析 p 開始標(biāo)簽的時(shí)候,stack 中存儲(chǔ)的是 p 標(biāo)簽父級(jí)元素的描述對(duì)象。
解析 </p>
解析結(jié)束標(biāo)簽,做回退操作。
遇到開始標(biāo)簽就生成元素,勾勒上下文關(guān)系 parent、children 等,每當(dāng)遇到一個(gè)非一元標(biāo)簽的結(jié)束標(biāo)簽時(shí),都會(huì)回退 currentParent 變量的值為之前的值,這樣就修正了當(dāng)前正在解析的元素的父級(jí)元素。
chars 鉤子函數(shù)
當(dāng)然在我們的代碼中肯定不止是開始和結(jié)束標(biāo)簽,還會(huì)有文本。當(dāng)遇到文本時(shí),就會(huì)調(diào)用 chars 鉤子函數(shù)。
①,首先判斷 currentParent(指向的是當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)) 變量是否存在,不存在就說明,說明 1:只有文本節(jié)點(diǎn)。2:文本在根元素之外。這兩種情況都會(huì)警告 ?? 提醒,接觸后面的操作。
②,第二個(gè)判斷主要是解決 ie textarea 占位符的問題。issue
③,判斷當(dāng)前元素未使用 v-pre 指令,text 不為空,使用 parseText 函數(shù)成功解析當(dāng)前文本節(jié)點(diǎn)的內(nèi)容。這里的重點(diǎn)在于 parseText 函數(shù),parseText 函數(shù)的作用就是用來解析如果我們的文本包含了字面量表達(dá)式。例如:
<div>1111: {{ text }}</div>
這樣的文本就會(huì)解析成如下的一個(gè)描述對(duì)象, 包含 expression 、tokens (包含原始的文本)。
解析完之后會(huì)生成一個(gè) type = 2 的描述對(duì)象:
child = { type: 2, expression: res.expression, tokens: res.tokens, text: text };
④,如果使用了 v-pre || test 為空 || parseText 解析失敗,那么就會(huì)生成一個(gè) type = 3 的存文本描述對(duì)象。
child = { type: 1, text: text };
⑤,最后將解析到描述對(duì)象,添加到當(dāng)前父元素的 children 列表中,注意:這里之前說明過因?yàn)槲覀兊恼麄€(gè) template 是不能是純文本的,必須由根元素,所以如果是文本節(jié)點(diǎn),一點(diǎn)是會(huì)有父元素的。
chars: function chars (text, start, end) { // ① if (!currentParent) { { if (text === template) { ...警告 } else if ((text = text.trim())) { ...警告 } } return } // ② if (isIE && currentParent.tag === 'textarea' && currentParent.attrsMap.placeholder === text ) { return } var children = currentParent.children; ... if (text) { ... // ③ if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { child = { type: 2, expression: res.expression, tokens: res.tokens, text: text }; // ④ } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { child = { type: 3, text: text }; } // ⑤ if (child) { ... children.push(child); } } },
到這里文本節(jié)點(diǎn)的解析完成。接下來看看注釋解析的鉤子函數(shù)。
commit 鉤子函數(shù)
當(dāng)我們配置了 options.comments = true ,也就意味著我們需要保留我們的注釋,這個(gè)配置需要我們手動(dòng)開啟,開啟后就會(huì)在頁面渲染后保留注釋。
注意:如果開啟了保留注釋匹配后,瀏覽器會(huì)保留注釋。但是可能對(duì)布局產(chǎn)生影響,尤其是對(duì)行內(nèi)元素的影響。為了消除這些影響帶來的問題,好的做法是將它們?nèi)サ簟?/p>
注釋的解析比較簡單,就是創(chuàng)建注釋節(jié)點(diǎn),然后添加當(dāng)前父元素的子階段列表中。要注意的是純文本節(jié)點(diǎn)和注釋節(jié)點(diǎn)的描述對(duì)象的 type 都是 3,不同的是注釋節(jié)點(diǎn)的元素描述對(duì)象擁有 isComment 屬性,并且該屬性的值為 true,目的就是用來與普通文本節(jié)點(diǎn)作區(qū)分的。
shouldKeepComment: options.comments, if (textEnd === 0) { ... if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3); } ... } comment: function comment (text, start, end) { if (currentParent) { var child = { type: 3, text: text, isComment: true }; ... currentParent.children.push(child); } }
到這里在生成 AST 過程中的 四個(gè)鉤子函數(shù)已經(jīng)全部講完。但是 Vue 本身在對(duì)元素做處理的時(shí)候的時(shí)候肯定不會(huì)是這么簡單的,因?yàn)檫@處理的過程中還要處理一元標(biāo)簽、靜態(tài)屬性、動(dòng)態(tài)屬性等。
番外(可跳過)
這一小節(jié)注意是看看在生成 AST 過程中的一些重要的工具函數(shù)。
createASTElement 函數(shù)
創(chuàng)建元素的描述對(duì)象。
function createASTElement ( tag, attrs, parent ) { return { type: 1, tag: tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, parent: parent, children: [] } }
指令解析相關(guān)的正則
前面也講到關(guān)于一些標(biāo)簽匹配相關(guān)的正則。其實(shí)這些正則大家在平時(shí)的項(xiàng)目中有涉及也可以用起來,畢竟這些正則是經(jīng)過千萬人測試的。
var onRE = /^@|^v-on:/; var dirRE = /^v-|^@|^:|^#/; var forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/; var forIteratorRE = /,([^,}]]*)(?:,([^,}]]*))?$/; var stripParensRE = /^(|)$/g; var dynamicArgRE = /^[.*]$/; var argRE = /:(.*)$/; var bindRE = /^:|^.|^v-bind:/; var modifierRE = /.[^.]]+(?=[^]]*$)/g;
onRE
匹配已字符@或者v-on開頭的字符串,檢測標(biāo)簽屬性是否是監(jiān)聽事件的指令。
var onRE = /^@|^v-on:/;
dirRE
匹配v-、@、:、#開頭的字符串,檢測屬性名是否是指令。v-開頭的屬性統(tǒng)統(tǒng)都認(rèn)為是指令。@字符是 v-on 的縮寫。:是 v-bind 的縮寫。#是 v-slot 的縮寫。
var dirRE = /^v-|^@|^:|^#/;
forAliasRE
匹配v-for屬性的值,目的是捕獲 in 或者 of 前后的字符串。
var forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
forIteratorRE
這個(gè)也是用來匹配v-for屬性的,不同的是,這里是匹配遍歷時(shí)的 value 、 key 、index 。
var forIteratorRE = /,([^,}]]*)(?:,([^,}]]*))?$/;
stripParensRE
匹配以字符(開頭、)結(jié)尾的字符串。作用是配合上面的正則對(duì)字符進(jìn)行處理(、)。
var stripParensRE = /^(|)$/g;
argRE
匹配指令中的參數(shù)。作用是捕獲指令中的參數(shù)。常見的就是指令中的修飾符。
var argRE = /:(.*)$/;
bindRE
匹配:、.、v-bind:開頭的字符串。作用是檢查屬性是否是綁定。
var bindRE = /^:|^.|^v-bind:/;
modifierRE
匹配修飾符。主要作用是判斷是否有修飾符。
var modifierRE = /.[^.]]+(?=[^]]*$)/g;
parseText 函數(shù)
這個(gè)函數(shù)的作用是解析 text,在上面講 chars 鉤子函數(shù)的時(shí)候也說到這個(gè)函數(shù)。函數(shù)有兩個(gè)參數(shù)text、delimiters。delimiters參數(shù)作用就是:改變純文本插入分隔符。例如:
delimiters: ['${', '}'], // 模板 <div>{{ text }}</div>
模板會(huì)被編譯成這樣。
在 parseText 函數(shù)中,重點(diǎn)邏輯是開啟一個(gè) while 循環(huán),使用 tagRE 正則匹配文本內(nèi)容,并將匹配結(jié)果保存在 match 變量中,直到匹配失敗循環(huán)才會(huì)終止,這時(shí)意味著所有的字面量表達(dá)式都已經(jīng)處理完畢了。
while ((match = tagRE.exec(text))) { index = match.index; if (index > lastIndex) { rawTokens.push(tokenValue = text.slice(lastIndex, index)); tokens.push(JSON.stringify(tokenValue)); } var exp = parseFilters(match[1].trim()); tokens.push(("_s(" + exp + ")")); rawTokens.push({ '@binding': exp }); lastIndex = index + match[0].length; }
例如有一段這樣的 template:
// text: '小白', // message: '好久不見' <div>hello, {{ text }},{{ message }}</div>
會(huì)被解析成如下 AST:
closeElement 函數(shù)
這個(gè)函數(shù)會(huì)在解析非一元開始標(biāo)簽和解析結(jié)束標(biāo)簽的時(shí)候調(diào)用,主要作用有兩個(gè):
對(duì)數(shù)據(jù)狀態(tài)進(jìn)行還原,
調(diào)用后置處理轉(zhuǎn)換鉤子函數(shù)。
整體流程
Vue 編譯三部曲第一步parse的整個(gè)流程已經(jīng)講述完畢,我們看著源代碼可能決定很相似,但假如只是抽離主流程的話,還是比較簡單的。parse的目的是將開發(fā)者寫的template模板字符串轉(zhuǎn)換成抽象語法樹 AST ,AST 就這里來說就是一個(gè)樹狀結(jié)構(gòu)的 JavaScript 對(duì)象,整個(gè)內(nèi)容就是描述上下關(guān)系。那么整個(gè)parse的過程是利用很多正則表達(dá)式順序解析模板,當(dāng)解析到開始標(biāo)簽、閉合標(biāo)簽、文本的時(shí)候都會(huì)分別執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù),來達(dá)到構(gòu)造 AST 樹的目的。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/127821.html
實(shí)踐是所有展示最好的方法,因此我覺得可以不必十分細(xì)致的,但我們的展示卻是整體的流程、輸入和輸出。現(xiàn)在我們就看看Vue 的指令、內(nèi)置組件等。也就是第二篇,模型樹優(yōu)化。 分析了 Vue 編譯三部曲的第一步,「如何將 template 編譯成 AST ?」上一篇已經(jīng)介紹,但我們還是來總結(jié)回顧下,parse 的目的是將開發(fā)者寫的 template 模板字符串轉(zhuǎn)換成抽象語法樹 AST ,AST 就這里...
摘要:具體可以查看抽象語法樹。而則是帶緩存的編譯器,同時(shí)以及函數(shù)會(huì)被轉(zhuǎn)換成對(duì)象。會(huì)用正則等方式解析模板中的指令等數(shù)據(jù),形成語法樹。是將語法樹轉(zhuǎn)化成字符串的過程,得到結(jié)果是的字符串以及字符串。里面的節(jié)點(diǎn)與父節(jié)點(diǎn)的結(jié)構(gòu)類似,層層往下形成一棵語法樹。 寫在前面 因?yàn)閷?duì)Vue.js很感興趣,而且平時(shí)工作的技術(shù)棧也是Vue.js,這幾個(gè)月花了些時(shí)間研究學(xué)習(xí)了一下Vue.js源碼,并做了總結(jié)與輸出。 文...
摘要:注意看注釋很粗很簡單,我就是一程序員姓名,年齡,請(qǐng)聯(lián)系我吧是否保留注釋定義分隔符,默認(rèn)為對(duì)于轉(zhuǎn)成,則需要先獲取,對(duì)于這部分內(nèi)容,做一個(gè)簡單的分析,具體的請(qǐng)自行查看源碼。其中的負(fù)責(zé)修改以及截取剩余模板字符串。 通過查看vue源碼,可以知道Vue源碼中使用了虛擬DOM(Virtual Dom),虛擬DOM構(gòu)建經(jīng)歷 template編譯成AST語法樹 -> 再轉(zhuǎn)換為render函數(shù) 最終返回...
摘要:問簡述一下的編譯過程先上一張圖大致看一下整個(gè)流程從上圖中我們可以看到是從后開始進(jìn)行中整體邏輯分為三個(gè)部分解析器將模板字符串轉(zhuǎn)換成優(yōu)化器對(duì)進(jìn)行靜態(tài)節(jié)點(diǎn)標(biāo)記,主要用來做虛擬的渲染優(yōu)化代碼生成器使用生成函數(shù)代碼字符串開始前先解釋一下抽象 20190215問 簡述一下Vue.js的template編譯過程? 先上一張圖大致看一下整個(gè)流程showImg(https://image-static....
直接進(jìn)入核心現(xiàn)在說說baseCompile核心代碼: //`createCompilerCreator`allowscreatingcompilersthatusealternative //parser/optimizer/codegen,e.gtheSSRoptimizingcompiler. //Herewejustexportadefaultcompilerusingthede...
閱讀 561·2023-03-27 18:33
閱讀 750·2023-03-26 17:27
閱讀 647·2023-03-26 17:14
閱讀 603·2023-03-17 21:13
閱讀 537·2023-03-17 08:28
閱讀 1823·2023-02-27 22:32
閱讀 1315·2023-02-27 22:27
閱讀 2199·2023-01-20 08:28