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