經過查看vue源碼,能夠知道Vue源碼中使用了虛擬DOM(Virtual Dom),虛擬DOM構建經歷 template編譯成AST語法樹 -> 再轉換爲render函數 最終返回一個VNode(VNode就是Vue的虛擬DOM節點) 。
本文經過對Vue源碼中的AST轉化部分進行簡單提取,返回靜態的AST結構(不考慮兼容性及屬性的具體解析)。並最終根據一個實例的template轉化爲最終的AST結構。html
在Vue的mount過程當中,template會被編譯成AST語法樹,AST是指抽象語法樹(abstract syntax tree或者縮寫爲AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式。vue
首先、定義一個簡單的html DOM結構、其中包括比較常見的標籤、文本以及註釋,用來生成AST結構。node
<div id="app" class="demo"> <!-- 注意看註釋 --> <p> <b>很粗</b> </p> 很簡單,我就是一程序員 <br/> <h1> 姓名:{{name}},年齡:{{age}}, 請聯繫我吧 </h1> </div> <script> var vm = new Vue({ el: '#app', // template: '#template', // template: 'string template', // template: document.querySelector('#template'), data () { return { name: 'Jeffery', age: '26' } }, comments: true, // 是否保留註釋 // delimiters: ['{', '}'] // 定義分隔符,默認爲"{{}}" }) </script>
對於轉成AST,則須要先獲取template,對於這部份內容,作一個簡單的分析,具體的請自行查看Vue源碼。
具體目錄請參考: '/src/platforms/web/entry-runtime-with-compiler'
從vue官網中知道,vue提供了兩個版本,完整版和只包含運行時版,差異是完整版包含編譯器,就是將template模板編譯成AST,再轉化爲render函數的過程,所以只包含運行時版必須提供render函數。
注意:此到處理比較簡單,只是爲了獲取template,以便用於生成AST。程序員
function Vue (options) { // 若是沒有提供render函數,則處理template,不然直接使用render函數 if (!options.render) { let template = options.template; // 若是提供了template模板 if (template) { // template: '#template', // template: '<div></div>', if (typeof template === 'string') { // 若是爲'#template' if (template.charAt(0) === '#') { let tpl = query(template); template = tpl ? tpl.innerHTML : ''; } // 不然不作處理,如:'<div></div>' } else if (template.nodeType) { // 若是模板爲DOM節點,如:template: document.querySelector('#template') // 好比:<script type="text/x-template" id="template"></script> template = template.innerHTML; } } else if (options.el) { // 若是沒有模板,則使用el template = getOuterHTML(query(options.el)); } if (template) { // 將template模板編譯成AST(此處省略一系列函數、參數處理過程,具體見下圖及源碼) let ast = null; ast = parse(template, options); console.log(ast) } } }
能夠看出:在options中,vue默認先使用render函數,若是沒有提供render函數,則會使用template模板,最後再使用el,經過解析模板編譯AST,最終轉化爲render。
其中函數以下:web
function query (el) { if (typeof el === 'string') { var selected = document.querySelector(el); if (!selected) { console.error('Cannot find element: ' + el); } return selected; } return el; } function getOuterHTML (el) { if (el.outerHTML) { return el.outerHTML; } else { var dom = document.createElement('div'); dom.appendChild(el.cloneNode(true)); return dom.innerHTML; } }
對於定義組件模板形式,能夠參考下這篇文章express
說了這麼多,也不廢話了,下面重點介紹template編譯成AST的過程。
根據源碼,先定義一些基本工具方法,以及對相關html標籤進行分類處理等。bash
// script、style、textarea標籤 function isPlainTextElement (tag) { let tags = { script: true, style: true, textarea: true } return tags[tag] } // script、style標籤 function isForbiddenTag (tag) { let tags = { script: true, style: true } return tags[tag] } // 自閉和標籤 function isUnaryTag (tag) { let strs = `area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr`; let tags = makeMap(strs); return tags[tag]; } // 結束標籤能夠省略"/" function canBeLeftOpenTag (tag) { let strs = `colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source`; let tags = makeMap(strs); return tags[tag]; } // 段落標籤 function isNonPhrasingTag (tag) { let strs = `address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track`; let tags = makeMap(strs); return tags[tag]; } // 結構:如 # { # script: true, # style: true # } function makeMap(strs) { let tags = strs.split(','); let o = {} for (let i = 0; i < tags.length; i++) { o[tags[i]] = true; } return o; }
定義正則以下:app
// 匹配屬性 const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ const ncname = '[a-zA-Z_][\\w\\-\\.]*' const qnameCapture = `((?:${ncname}\\:)?${ncname})` // 匹配開始標籤開始部分 const startTagOpen = new RegExp(`^<${qnameCapture}`) // 匹配開始標籤結束部分 const startTagClose = /^\s*(\/?)>/ // 匹配結束標籤 const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) // 匹配註釋 const comment = /^<!\--/ // 匹配默認的分隔符 "{{}}" const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
定義標籤結構:dom
function createASTElement (tag, attrs, parent) { // attrs: # [ # { # name: 'id', # value: 'app' # }, # { # name: 'class', # value: 'demo' # } # ] let attrsMap = {} for (let i = 0, len = attrs.length; i < len; i++) { attrsMap[attrs[i].name] = attrs[i].value; } // attrsMap: # { # id: 'app', # class: 'demo' # } return { type: 1, tag, attrsList: attrs, attrsMap: attrsMap, parent, children: [] } }
主要的parse具體代碼以下:ide
function parse (template, options) { let root; // 最終返回的AST let currentParent; // 設置當前標籤的父節點 let stack = []; // 維護一個棧,保存解析過程當中的開始標籤,用於匹配結束標籤 // 解析模板的具體實現 parseHTML(template, { expectHTML: true, shouldKeepComment: options.comments, // 是否保存註釋 delimiters: options.delimiters, // 自定義的分隔符 start (tag, attrs, unary) {( // 處理開始標籤,解析的開始標籤入棧,設置children以及parent等(其中的屬性解析請查看源碼) let element = createASTElement(tag, attrs, currentParent); // 若是tag爲script/style標籤,設置屬性,返回的AST中不含該標籤元素結構 if (isForbiddenTag(tag)) { element.forbidden = true; console.error('Templates should only be responsible for mapping the state to the ' + 'UI. Avoid placing tags with side-effects in your templates, such as ' + "<" + tag + ">" + ', as they will not be parsed.') } // 設置根元素節點 if (!root) { root = element; } // 設置元素的父節點,將當前元素的添加到父節點的children中 if (currentParent && !element.forbidden) { currentParent.children.push(element); element.parent = currentParent; } // 若是不是自閉和標籤(沒有對應的結束標籤),則須要將當前tag入棧,用於匹配結束標籤時,調用end方法匹配最近的標籤,同時設置父節點爲當前元素 if (!unary) { currentParent = element; stack.push(element); } }, end () { // 將匹配結束的標籤出棧,修改父節點爲以前上一個元素 let element = stack.pop(); currentParent = stack[stack.length - 1]; }, chars (text) { // 保存文本 if (!currentParent) { console.error('Component template requires a root element, rather than just text.'); } else { const children = currentParent.children; if (text) { let res; // 若是文本節點包含表達式 if (res = parseText(text, opt.delimiters)) { children.push({ type: 2, expression: res.expression, tokens: res.tokens, text }) } else { children.push({ type: 3, text }) } } } }, comment (text) { // 保存註釋 if (currentParent) { currentParent.children.push({ type: 3, text, isComment: true }) } } }) return root; }
從上面的能夠看出:在parse函數中,主要用來解析template模板,造成AST結構,生成一個最終的root根元素,並返回。
而對於標籤、文本、註釋type也是不一樣的。
其中:
標籤:type爲1
含有表達式文本:type爲2
不含表達式文本:type爲3
註釋: type爲3,同時isComment爲true
同時,options參數對象上添加了start、end、chars和comment四個方法,用來處理當匹配到開始標籤、結束標籤、文本以及註釋時,匹配對應的開始標籤,設置相應的currentParent以及parent等,生成成AST。
當調用parseHTML後,會在處理標籤的不一樣狀況下,調用對應的這四個方法。
在start中:每次處理開始標籤時,會設置一個root節點(只會設置一次),當標籤而且不是自閉合標籤時(沒有對應的結束標籤),加入stack中,並將當前元素設置爲currentParent,一層層往內匹配,最終的currentParent爲最內層的元素標籤,並將當前元素保存到爲currentParent的children中及parent爲currentParent。
在end中:在stack中找到最近的相同標籤(棧中的最後一個),設置爲currentParent,並出棧,一層層往外匹配。
形如: html:<div><p></p></div> stack:['div', 'p'] pop: p => pop: div
而對於chars和comment,則分別是保存文本以及註釋到對應的currentParent的children中。
其中parseHTML:
// 定義幾個全局變量 let stack = []; // 保存開始標籤tag,和上面相似 let lastTag; // 保存前一個標籤,相似於currentParent let index = 0; // template開始解析索引 let html; // 剩餘的template模板 let opt; // 保存對options的引用,方便調用start、end、chars、comment方法 function parseHTML (template, options) { html = template; opt = options; // 不斷循環解析html,直到爲"" while(html) { // 若是標籤tag不是script/style/textarea if (!lastTag || !isPlainTextElement(lastTag)) { // 剛開始或tag不爲script/style/textarea let textEnd = html.indexOf('<'); if (textEnd === 0) { // html以"<"開始 // 處理html註釋 if (html.match(comment)) { let commentEnd = html.indexOf('-->'); if (commentEnd >= 0) { if (opt.shouldKeepComment && opt.comment) { // 保存註釋內容 opt.comment(html.substring(4, commentEnd)) } // 調整index以及html advance(commentEnd + 3); continue; } } // 處理 html條件註釋, 如<![if !IE]> // 處理html聲明Doctype // 處理開始標籤startTaga const startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); continue; } // 匹配結束標籤endTag const endTagMatch = html.match(endTag); if (endTagMatch) { // 調整index以及html advance(endTagMatch[0].length); // 處理結束標籤 parseEndTag(endTagMatch[1]); continue; } } let text; if (textEnd > 0) { // html爲純文本,須要考慮文本中含有"<"的狀況,此處省略,請自行查看源碼 text = html.slice(0, textEnd); // 調整index以及html advance(textEnd); } if (textEnd < 0) { // htlml以文本開始 text = html; html = ''; } // 保存文本內容 if (opt.chars) { opt.chars(text); } } else { // tag爲script/style/textarea let stackedTag = lastTag.toLowerCase(); let tagReg = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'); // 簡單處理下,詳情請查看源碼 let match = html.match(tagReg); if (match) { let text = match[1]; if (opt.chars) { // 保存script/style/textarea中的內容 opt.chars(text); } // 調整index以及html advance(text.length + match[2].length); // 處理結束標籤</script>/</style>/</textarea> parseEndTag(stackedTag); } } } }
定義advance:
// 修改模板不斷解析後的位置,以及截取模板字符串,保留未解析的template function advance (n) { index += n; html = html.substring(n) }
在parseHTML中,能夠看到:經過不斷循環,修改當前未知的索引index以及不斷截取html模板,並分狀況處理、解析,直到最後剩下空字符串爲止。
其中的advance負責修改index以及截取剩餘html模板字符串。
下面主要看看解析開始標籤和結束標籤:
function parseStartTag () { let start = html.match(startTagOpen); if (start) { // 結構:["<div", "div", index: 0, groups: undefined, input: "..."] let match = { tagName: start[1], attrs: [], start: index } // 調整index以及html advance(start[0].length); // 循環匹配屬性 let end, attr; while (!(end = html.match(startTagClose))&& (attr = html.match(attribute))) { // 結構:["id="app"", "id", "=", "app", undefined, undefined, groups: undefined, index: 0, input: "..."] advance(attr[0].length); match.attrs.push(attr); } // 匹配到開始標籤的結束位置 if (end) { match.unarySlash = end[1]; // end[1]匹配的是"/",如<br/> // 調整index以及html advance(end[0].length) match.end = index; return match; } } }
在parseStartTag中,將開始標籤處理成特定的結構,包括標籤名、全部的屬性名,開始位置、結束位置及是不是自閉和標籤。
結構如:{
tagName,
attrs,
start,
end,
unarySlash
}
function handleStartTag(match) { const tagName = match.tagName; const unarySlash = match.unarySlash; if (opt.expectHTML) { if (lastTag === 'p' && isNonPhrasingTag(tagName)) { // 若是p標籤包含了段落標籤,如div、h一、h2等 // 形如: <p><h1></h1></p> // 與parseEndTag中tagName爲p時相對應,處理</p>,添加<p> // 處理結果: <p></p><h1></h1><p></p> parseEndTag(lastTag); } if (canBeLeftOpenTag(tagName) && lastTag === tagName) { // 若是標籤閉合標籤能夠省略"/" // 形如:<li><li> // 處理結果: <li></li> parseEndTag(tagName); } } // 處理屬性結構(name和vulue形式) let attrs = []; attrs.length = match.attrs.length; for (let i = 0, len = match.attrs.length; i < len; i++) { attrs[i] = { name: match.attrs[i][2], value: match.attrs[i][3] } } // 判斷是否是自閉和標籤,如<br> let unary = isUnaryTag(tagName) || !!unarySlash; // 若是不是自閉合標籤,保存到stack中,用於endTag匹配, if (!unary) { stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs }) // 從新設置上一個標籤 lastTag = tagName; } if (opt.start) { opt.start(tagName, attrs, unary) } }
將開始標籤處理成特定結構後,再經過handleStartTag,將attrs進一步處理,成name、value結構形式。
結構如:attrs: [
{
name: 'id', value: 'app'
}
]
保持和以前處理一致,非自閉和標籤時,從外標籤往內標籤,一層層入棧,須要保存到stack中,並設置lastTag爲當前標籤。
function parseEndTag (tagName) { let pos = 0; // 匹配stack中開始標籤中,最近的匹配標籤位置 if (tagName) { tagName = tagName.toLowerCase(); for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === tagName) { break; } } } // 若是能夠匹配成功 if (pos >= 0) { let i = stack.length - 1; if (i > pos || !tagName) { console.error(`tag <${stack[i - 1].tag}> has no matching end tag.`) } // 若是匹配正確: pos === i if (opt.end) { opt.end(); } // 將匹配成功的開始標籤出棧,並修改lastTag爲以前的標籤 stack.length = pos; lastTag = pos && stack[stack.length - 1].tagName; } else if (tagName === 'br') { // 處理: </br> if (opt.start) { opt.start(tagName, [], true) } } else if (tagName === 'p') { // 處理上面說的狀況:<p><h1></h1></p> if (opt.start) { opt.start(tagName, [], false); } if (opt.end) { opt.end(); } } }
parseEndTag中,處理結束標籤時,須要一層層往外,在stack中找到當前標籤最近的相同標籤,獲取stack中的位置,若是標籤匹配正確,通常爲stack中的最後一個(不然缺乏結束標籤),若是匹配成功,將棧中的匹配標籤出棧,並從新設置lastTag爲棧中的最後一個。
注意:須要特殊處理br或p標籤,標籤在stack中找不到對應的匹配標籤,須要單獨保存到AST結構中,而</p>標籤主要是爲了處理特殊狀況,和以前開始標籤中處理相關,此時會多一個</p>標籤,在stack中最近的標籤不是p,也須要單獨保存到AST結構中。
差點忘了還有一個parseText函數。
其中parseText:
function parseText (text, delimiters) { let open; let close; let resDelimiters; // 處理自定義的分隔符 if (delimiters) { open = delimiters[0].replace(regexEscapeRE, '\\$&'); close = delimiters[1].replace(regexEscapeRE, '\\$&'); resDelimiters = new RegExp(open + '((?:.|\\n)+?)' + close, 'g'); } const tagRE = delimiters ? resDelimiters : defaultTagRE; // 沒有匹配,文本中不含表達式,返回 if (!tagRE.test(text)) { return; } const tokens = [] const rawTokens = []; let lastIndex = tagRE.lastIndex = 0; let index; let match; // 循環匹配本文中的表達式 while(match = tagRE.exec(text)) { index = match.index; if (index > lastIndex) { let value = text.slice(lastIndex, index); tokens.push(JSON.stringify(value)); rawTokens.push(value) } // 此處須要處理過濾器,暫不處理,請查看源碼 let exp = match[1].trim(); tokens.push(`_s(${exp})`); rawTokens.push({'@binding': exp}) lastIndex = index + match[0].length; } if (lastIndex < text.length) { let value = text.slice(lastIndex); tokens.push(JSON.stringify(value)); rawTokens.push(value); } return { expression: tokens.join('+'), tokens: rawTokens } }
最後,附上以上原理簡略分析圖:
<div id="app" class="demo"> <!-- 注意看註釋 --> <p> <b>很粗</b> </p> 很簡單,我就是一程序員 <br/> <h1> 姓名:{{name}},年齡:{{age}}, 請聯繫我吧 </h1> </div> 解析流程以下: 分析過程:tagName stack1 lastTag currentParent stack2 root children parent 操做 div div [div] div div [div] div div:[p] null 入棧 comment 註釋 ---> 保存到currentParent.children中 p p [div,p] p p [div,p] div p:[b] div 入棧 b b [div,p,b] b b [div,p,b] div b:[text] p 入棧 /b b [div,p] p p [div,p] div --- --- 出棧 /p p [div] div div [div] div --- --- 出棧 text 文本 ---> 通過處理後,保存到currentParent.children中 h1 h1 [div,h1] h1 h1 [div,h1] div h1:[text] div 入棧 text 文本 ---> 通過處理後,保存到currentParent.children中 /h1 h1 [div] div div [div] div --- --- 出棧 /div div [] null null [] div --- --- 出棧 最終:root = div:[p,h1]
最終AST結構以下:
以上是我根據vue源碼分析,抽出來的簡單的template轉化AST,文中如有什麼不對的地方請你們幫忙指正,本人最近也一直在學習Vue的源碼,但願可以拿出來與你們一塊兒分享經驗,接下來會繼續更新後續的源碼,若是以爲有須要能夠相互交流。