在Vue的mount過程當中,template會被編譯成AST語法樹,AST是指抽象語法樹(abstract syntax tree或者縮寫爲AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式。html
Vue的一個厲害之處就是利用Virtual DOM模擬DOM對象樹來優化DOM操做的一種技術或思路。
Vue源碼中虛擬DOM構建經歷 template編譯成AST語法樹 -> 再轉換爲render函數 最終返回一個VNode(VNode就是Vue的虛擬DOM節點)
本文經過對源碼中AST轉化部分進行簡單提取,由於源碼中轉化過程還須要進行各類兼容判斷,很是複雜,因此筆者對主要功能代碼進行提取,用了300-400行代碼完成對template轉化爲AST這個功能。下面用具體代碼進行分析。vue
function parse(template) { var currentParent; //當前父節點 var root; //最終返回出去的AST樹根節點 var stack = []; parseHTML(template, { start: function start(tag, attrs, unary) { ...... }, end: function end() { ...... }, chars: function chars(text) { ...... } }) return root }
第一步就是調用parse這個方法,把template傳進來,這裏假設template爲 <div id="app"><span>{{message}}</span></div>
git
而後聲明3個變量
currentParent -> 存放當前父元素,root -> 最終返回出去的AST樹根節點,stack -> 一個棧用來輔助樹的創建
接着調用parseHTML函數進行轉化,傳入template和options(包含3個方法 start,end,chars 等下用到這3個函數再進行解釋)接下來先看parseHTML這個方法github
function parseHTML(html, options) { var stack = []; //這裏和上面的parse函數同樣用到stack這個數組 不過這裏的stack只是爲了簡單存放標籤名 爲了和結束標籤進行匹配的做用 var isUnaryTag$$1 = isUnaryTag; //判斷是否爲自閉合標籤 var index = 0; var last; while (html) { // 第一次進入while循環時,因爲字符串以<開頭,因此進入startTag條件,並進行AST轉換,最後將對象彈入stack數組中 last = html; var textEnd = html.indexOf('<'); if (textEnd === 0) { // 此時字符串是否是以<開頭 // End tag: var endTagMatch = html.match(endTag); if (endTagMatch) { var curIndex = index; advance(endTagMatch[0].length); parseEndTag(endTagMatch[1], curIndex, index); continue } // Start tag: // 匹配起始標籤 var startTagMatch = parseStartTag(); //處理後獲得match if (startTagMatch) { handleStartTag(startTagMatch); continue } } // 初始化爲undefined 這樣安全且字符數少一點 var text = (void 0), rest = (void 0), next = (void 0); if (textEnd >= 0) { // 截取<字符索引 => </div> 這裏截取到閉合的< rest = html.slice(textEnd); //截取閉合標籤 // 處理文本中的<字符 // 獲取中間的字符串 => {{message}} text = html.substring(0, textEnd); //截取到閉合標籤前面部分 advance(textEnd); //切除閉合標籤前面部分 } // 當字符串沒有<時 if (textEnd < 0) { text = html; html = ''; } // // 處理文本 if (options.chars && text) { options.chars(text); } } }
函數進入while循環對html進行獲取<
標籤索引 var textEnd = html.indexOf('<');
若是textEnd === 0 說明當前是標籤<xxx>或者</xxx> 再用正則匹配是否當前是結束標籤</xxx>。var endTagMatch = html.match(endTag);
匹配不到那麼就是開始標籤,調用parseStartTag()函數解析。express
function parseStartTag() { //返回匹配對象 var start = html.match(startTagOpen); // 正則匹配 if (start) { var match = { tagName: start[1], // 標籤名(div) attrs: [], // 屬性 start: index // 遊標索引(初始爲0) }; advance(start[0].length); var end, attr; while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { advance(attr[0].length); match.attrs.push(attr); } if (end) { advance(end[0].length); // 標記結束位置 match.end = index; //這裏的index 是在 parseHTML就定義 在advance裏面相加 return match // 返回匹配對象 起始位置 結束位置 tagName attrs } } }
該函數主要是爲了構建一個match對象,對象裏面包含tagName(標籤名),attrs(標籤的屬性),start(<
左開始標籤在template中的位置),end(>
右開始標籤在template中的位置) 如template = <div id="app"><div><span>{{message}}</span></div></div>
程序第一次進入該函數 匹配的是div標籤 因此tagName就是div
start:0 end:14 如圖:數組
接着把match返回出去 做爲調用handleStartTag的參數安全
var startTagMatch = parseStartTag(); //處理後獲得match if (startTagMatch) { handleStartTag(startTagMatch); continue }
接下來看handleStartTag這個函數:app
function handleStartTag(match) { var tagName = match.tagName; var unary = isUnaryTag$$1(tagName) //判斷是否爲閉合標籤 var l = match.attrs.length; var attrs = new Array(l); for (var i = 0; i < l; i++) { var args = match.attrs[i]; var value = args[3] || args[4] || args[5] || ''; attrs[i] = { name: args[1], value: value }; } if (!unary) { stack.push({tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs}); lastTag = tagName; } if (options.start) { options.start(tagName, attrs, unary, match.start, match.end); } }
函數中分爲3部分 第一部分是for循環是對attrs進行轉化,咱們從上一步的parseStartTag()獲得的match對象中的attrs屬性如圖ide
當時attrs是上面圖這樣子滴 咱們經過這個循環把它轉化爲只帶name 和 value這2個屬性的對象 如圖:函數
接着判斷若是不是自閉合標籤,把標籤名和屬性推入棧中(注意 這裏的stack這個變量在parseHTML中定義,做用是爲了存放標籤名 爲了和結束標籤進行匹配的做用。)接着調用最後一步 options.start 這裏的options就是咱們在parse函數中 調用parseHTML是傳進來第二個參數的那個對象(包含start end chars 3個方法函數) 這裏開始看options.start這個函數的做用:
start: function start(tag, attrs, unary) { var element = { type: 1, tag: tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), parent: currentParent, children: [] }; processAttrs(element); if (!root) { root = element; } if(currentParent){ currentParent.children.push(element); element.parent = currentParent; } if (!unary) { currentParent = element; stack.push(element); } }
這個函數中 生成element對象 再鏈接元素的parent 和 children節點 最終push到棧中
此時棧中第一個元素生成 如圖:
完成了while循環的第一次執行,進入第二次循環執行,這個時候html變成<span>{{message}}</span></div>
接着截取到<span> 處理過程和第一次一致 通過此次循環stack中元素如圖:
接着繼續執行第三個循環 這個時候是處理文本節點了 {{message}}
// 初始化爲undefined 這樣安全且字符數少一點 var text = (void 0), rest = (void 0), next = (void 0); if (textEnd >= 0) { // 截取<字符索引 => </div> 這裏截取到閉合的< rest = html.slice(textEnd); //截取閉合標籤 // 處理文本中的<字符 // 獲取中間的字符串 => {{message}} text = html.substring(0, textEnd); //截取到閉合標籤前面部分 advance(textEnd); //切除閉合標籤前面部分 } // 當字符串沒有<時 if (textEnd < 0) { text = html; html = ''; } // 另一個函數 if (options.chars && text) { options.chars(text); }
這裏的做用就是把文本提取出來 調用options.chars這個函數 接下來看options.chars
chars: function chars(text) { if (!currentParent) { //若是沒有父元素 只是文本 return } var children = currentParent.children; //取出children // text => {{message}} if (text) { var expression; if (text !== ' ' && (expression = parseText(text))) { // 將解析後的text存進children數組 children.push({ type: 2, expression: expression, text: text }); } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { children.push({ type: 3, text: text }); } } } })
這裏的主要功能是判斷文本是{{xxx}}仍是簡單的文本xxx,若是是簡單的文本 push進父元素的children裏面,type設置爲3,若是是字符模板{{xxx}},調用parseText轉化。如這裏的{{message}}
轉化爲 _s(message)
(加上_s是爲了AST的下一步轉爲render函數,本文中暫時不會用到。) 再把轉化後的內容push進children。
又走完一個循環了,這個時候html = </span></div>
剩下2個結束標籤進行匹配了
var endTagMatch = html.match(endTag); if (endTagMatch) { var curIndex = index; advance(endTagMatch[0].length); parseEndTag(endTagMatch[1], curIndex, index); continue }
接下來看parseEndTag這個函數 傳進來了標籤名 開始索引和結束索引
function parseEndTag(tagName, start, end) { var pos, lowerCasedTagName; if (tagName) { lowerCasedTagName = tagName.toLowerCase(); } // Find the closest opened tag of the same type if (tagName) { // 獲取最近的匹配標籤 for (pos = stack.length - 1; pos >= 0; pos--) { // 提示沒有匹配的標籤 if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { // If no tag name is provided, clean shop pos = 0; } if (pos >= 0) { // Close all the open elements, up the stack for (var i = stack.length - 1; i >= pos; i--) { if (options.end) { options.end(stack[i].tag, start, end); } } // Remove the open elements from the stack stack.length = pos; lastTag = pos && stack[pos - 1].tag; }
這裏首先找到棧中對應的開始標籤的索引pos,再從該索引開始到棧頂的因此元素調用options.end這個函數
end: function end() { // pop stack stack.length -= 1; currentParent = stack[stack.length - 1]; },
把棧頂元素出棧,由於這個元素已經匹配到結束標籤了,再把當前父元素更改。終於走完了,把html的內容循環完,最終return root 這個root就是咱們所要獲得的AST
這只是Vue的冰山一角,文中有什麼不對的地方請你們幫忙指正,本人最近也一直在學習Vue的源碼,但願可以拿出來與你們一塊兒分享經驗,接下來會繼續更新後續的源碼,若是以爲有幫忙請給個Star哈
github地址爲:https://github.com/zwStar/vue... 歡迎各位star或issues