經由歷程檢察vue源碼,可以曉得Vue源碼中運用了假造DOM(Virtual Dom),假造DOM構建閱歷 template編譯成AST語法樹 -> 再轉換為render函數 終究返回一個VNode(VNode就是Vue的假造DOM節點) 。
本文經由歷程對Vue源碼中的AST轉化部份舉行簡樸提取,返回靜態的AST構造(不斟酌兼容性及屬性的詳細剖析)。並終究依據一個實例的template轉化為終究的AST構造。
什麼是AST
在Vue的mount歷程當中,template會被編譯成AST語法樹,AST是指籠統語法樹(abstract syntax tree或許縮寫為AST),或許語法樹(syntax tree),是源代碼的籠統語法構造的樹狀表現情勢。
代碼剖析
起首、定義一個簡樸的html DOM構造、个中包括比較罕見的標籤、文本以及解釋,用來天生AST構造。
<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。
个中函數以下:
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;
}
}
關於定義組件模板情勢,可以參考下這篇文章
說了這麼多,也不空話了,下面重點引見template編譯成AST的歷程。
依據源碼,先定義一些基礎東西要領,以及對相干html標籤舉行分類處置懲罰等。
// 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;
}
定義正則以下:
// 婚配屬性
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
定義標籤構造:
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詳細代碼以下:
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、h1、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的源碼,願望可以拿出來與人人一同分享履歷,接下來會繼承更新後續的源碼,假如以為有須要可以互相交換。