摘要:注意看注釋很粗很簡單,我就是一程序員姓名,年齡,請(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è)VNode(VNode就是Vue的虛擬DOM節(jié)點(diǎn)) 。
本文通過對(duì)Vue源碼中的AST轉(zhuǎn)化部分進(jìn)行簡單提取,返回靜態(tài)的AST結(jié)構(gòu)(不考慮兼容性及屬性的具體解析)。并最終根據(jù)一個(gè)實(shí)例的template轉(zhuǎn)化為最終的AST結(jié)構(gòu)。
在Vue的mount過程中,template會(huì)被編譯成AST語法樹,AST是指抽象語法樹(abstract syntax tree或者縮寫為AST),或者語法樹(syntax tree),是源代碼的抽象語法結(jié)構(gòu)的樹狀表現(xiàn)形式。
代碼分析首先、定義一個(gè)簡單的html DOM結(jié)構(gòu)、其中包括比較常見的標(biāo)簽、文本以及注釋,用來生成AST結(jié)構(gòu)。
很粗
很簡單,我就是一程序員
姓名:{{name}},年齡:{{age}}, 請(qǐng)聯(lián)系我吧
對(duì)于轉(zhuǎn)成AST,則需要先獲取template,對(duì)于這部分內(nèi)容,做一個(gè)簡單的分析,具體的請(qǐng)自行查看Vue源碼。
具體目錄請(qǐng)參考: "/src/platforms/web/entry-runtime-with-compiler"
從vue官網(wǎng)中知道,vue提供了兩個(gè)版本,完整版和只包含運(yùn)行時(shí)版,差別是完整版包含編譯器,就是將template模板編譯成AST,再轉(zhuǎn)化為render函數(shù)的過程,因此只包含運(yùn)行時(shí)版必須提供render函數(shù)。
注意:此處處理比較簡單,只是為了獲取template,以便用于生成AST。
function Vue (options) { // 如果沒有提供render函數(shù),則處理template,否則直接使用render函數(shù) if (!options.render) { let template = options.template; // 如果提供了template模板 if (template) { // template: "#template", // template: "", if (typeof template === "string") { // 如果為"#template" if (template.charAt(0) === "#") { let tpl = query(template); template = tpl ? tpl.innerHTML : ""; } // 否則不做處理,如:"" } else if (template.nodeType) { // 如果模板為DOM節(jié)點(diǎn),如:template: document.querySelector("#template") // 比如: template = template.innerHTML; } } else if (options.el) { // 如果沒有模板,則使用el template = getOuterHTML(query(options.el)); } if (template) { // 將template模板編譯成AST(此處省略一系列函數(shù)、參數(shù)處理過程,具體見下圖及源碼) let ast = null; ast = parse(template, options); console.log(ast) } } }
可以看出:在options中,vue默認(rèn)先使用render函數(shù),如果沒有提供render函數(shù),則會(huì)使用template模板,最后再使用el,通過解析模板編譯AST,最終轉(zhuǎn)化為render。
其中函數(shù)如下:
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; } }
對(duì)于定義組件模板形式,可以參考下這篇文章
說了這么多,也不廢話了,下面重點(diǎn)介紹template編譯成AST的過程。
根據(jù)源碼,先定義一些基本工具方法,以及對(duì)相關(guān)html標(biāo)簽進(jìn)行分類處理等。
// script、style、textarea標(biāo)簽 function isPlainTextElement (tag) { let tags = { script: true, style: true, textarea: true } return tags[tag] } // script、style標(biāo)簽 function isForbiddenTag (tag) { let tags = { script: true, style: true } return tags[tag] } // 自閉和標(biāo)簽 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]; } // 結(jié)束標(biāo)簽可以省略"/" function canBeLeftOpenTag (tag) { let strs = `colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source`; let tags = makeMap(strs); return tags[tag]; } // 段落標(biāo)簽 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]; } // 結(jié)構(gòu):如 # { # 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})` // 匹配開始標(biāo)簽開始部分 const startTagOpen = new RegExp(`^<${qnameCapture}`) // 匹配開始標(biāo)簽結(jié)束部分 const startTagClose = /^s*(/?)>/ // 匹配結(jié)束標(biāo)簽 const endTag = new RegExp(`^${qnameCapture}[^>]*>`) // 匹配注釋 const comment = /^"); if (commentEnd >= 0) { if (opt.shouldKeepComment && opt.comment) { // 保存注釋內(nèi)容 opt.comment(html.substring(4, commentEnd)) } // 調(diào)整index以及html advance(commentEnd + 3); continue; } } // 處理 html條件注釋, 如 // 處理html聲明Doctype // 處理開始標(biāo)簽startTaga const startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); continue; } // 匹配結(jié)束標(biāo)簽endTag const endTagMatch = html.match(endTag); if (endTagMatch) { // 調(diào)整index以及html advance(endTagMatch[0].length); // 處理結(jié)束標(biāo)簽 parseEndTag(endTagMatch[1]); continue; } } let text; if (textEnd > 0) { // html為純文本,需要考慮文本中含有"<"的情況,此處省略,請(qǐng)自行查看源碼 text = html.slice(0, textEnd); // 調(diào)整index以及html advance(textEnd); } if (textEnd < 0) { // htlml以文本開始 text = html; html = ""; } // 保存文本內(nèi)容 if (opt.chars) { opt.chars(text); } } else { // tag為script/style/textarea let stackedTag = lastTag.toLowerCase(); let tagReg = new RegExp("([sS]*?)(" + stackedTag + "[^>]*>)", "i"); // 簡單處理下,詳情請(qǐng)查看源碼 let match = html.match(tagReg); if (match) { let text = match[1]; if (opt.chars) { // 保存script/style/textarea中的內(nèi)容 opt.chars(text); } // 調(diào)整index以及html advance(text.length + match[2].length); // 處理結(jié)束標(biāo)簽// parseEndTag(stackedTag); } } } }
定義advance:
// 修改模板不斷解析后的位置,以及截取模板字符串,保留未解析的template function advance (n) { index += n; html = html.substring(n) }
在parseHTML中,可以看到:通過不斷循環(huán),修改當(dāng)前未知的索引index以及不斷截取html模板,并分情況處理、解析,直到最后剩下空字符串為止。
其中的advance負(fù)責(zé)修改index以及截取剩余html模板字符串。
下面主要看看解析開始標(biāo)簽和結(jié)束標(biāo)簽:
function parseStartTag () { let start = html.match(startTagOpen); if (start) { // 結(jié)構(gòu):[" // 調(diào)整index以及html advance(end[0].length) match.end = index; return match; } } }
在parseStartTag中,將開始標(biāo)簽處理成特定的結(jié)構(gòu),包括標(biāo)簽名、所有的屬性名,開始位置、結(jié)束位置及是否是自閉和標(biāo)簽。
結(jié)構(gòu)如:{
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標(biāo)簽包含了段落標(biāo)簽,如div、h1、h2等 // 形如: // 與parseEndTag中tagName為p時(shí)相對(duì)應(yīng),處理,添加// 處理結(jié)果:
parseEndTag(lastTag); } if (canBeLeftOpenTag(tagName) && lastTag === tagName) { // 如果標(biāo)簽閉合標(biāo)簽可以省略"/" // 形如:
將開始標(biāo)簽處理成特定結(jié)構(gòu)后,再通過handleStartTag,將attrs進(jìn)一步處理,成name、value結(jié)構(gòu)形式。
結(jié)構(gòu)如:attrs: [
{
name: "id", value: "app"
}
]
保持和之前處理一致,非自閉和標(biāo)簽時(shí),從外標(biāo)簽往內(nèi)標(biāo)簽,一層層入棧,需要保存到stack中,并設(shè)置lastTag為當(dāng)前標(biāo)簽。
function parseEndTag (tagName) { let pos = 0; // 匹配stack中開始標(biāo)簽中,最近的匹配標(biāo)簽位置 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(); } // 將匹配成功的開始標(biāo)簽出棧,并修改lastTag為之前的標(biāo)簽 stack.length = pos; lastTag = pos && stack[stack.length - 1].tagName; } else if (tagName === "br") { // 處理: if (opt.start) { opt.start(tagName, [], true) } } else if (tagName === "p") { // 處理上面說的情況: if (opt.start) { opt.start(tagName, [], false); } if (opt.end) { opt.end(); } } }
parseEndTag中,處理結(jié)束標(biāo)簽時(shí),需要一層層往外,在stack中找到當(dāng)前標(biāo)簽最近的相同標(biāo)簽,獲取stack中的位置,如果標(biāo)簽匹配正確,一般為stack中的最后一個(gè)(否則缺少結(jié)束標(biāo)簽),如果匹配成功,將棧中的匹配標(biāo)簽出棧,并重新設(shè)置lastTag為棧中的最后一個(gè)。
注意:需要特殊處理br或p標(biāo)簽,標(biāo)簽在stack中找不到對(duì)應(yīng)的匹配標(biāo)簽,需要多帶帶保存到AST結(jié)構(gòu)中,而
差點(diǎn)忘了還有一個(gè)parseText函數(shù)。
其中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 + "((?:.| )+?)" + close, "g"); } const tagRE = delimiters ? resDelimiters : defaultTagRE; // 沒有匹配,文本中不含表達(dá)式,返回 if (!tagRE.test(text)) { return; } const tokens = [] const rawTokens = []; let lastIndex = tagRE.lastIndex = 0; let index; let match; // 循環(huán)匹配本文中的表達(dá)式 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) } // 此處需要處理過濾器,暫不處理,請(qǐng)查看源碼 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 } }
最后,附上以上原理簡略分析圖:
解析流程如下: 分析過程: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 文本 ---> 經(jīng)過處理后,保存到currentParent.children中 h1 h1 [div,h1] h1 h1 [div,h1] div h1:[text] div 入棧 text 文本 ---> 經(jīng)過處理后,保存到currentParent.children中 /h1 h1 [div] div div [div] div --- --- 出棧 /div div [] null null [] div --- --- 出棧 最終:root = div:[p,h1]很粗
很簡單,我就是一程序員
姓名:{{name}},年齡:{{age}}, 請(qǐng)聯(lián)系我吧
最終AST結(jié)構(gòu)如下:
以上是我根據(jù)vue源碼分析,抽出來的簡單的template轉(zhuǎn)化AST,文中若有什么不對(duì)的地方請(qǐng)大家?guī)兔χ刚救俗罱惨恢痹趯W(xué)習(xí)Vue的源碼,希望能夠拿出來與大家一起分享經(jīng)驗(yàn),接下來會(huì)繼續(xù)更新后續(xù)的源碼,如果覺得有需要可以相互交流。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/95814.html
直接進(jìn)入核心現(xiàn)在說說baseCompile核心代碼: //`createCompilerCreator`allowscreatingcompilersthatusealternative //parser/optimizer/codegen,e.gtheSSRoptimizingcompiler. //Herewejustexportadefaultcompilerusingthede...
寫文章不容易,點(diǎn)個(gè)贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內(nèi)部詳情,讓我們一起學(xué)習(xí)吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請(qǐng)點(diǎn)擊 下面鏈接 或者 拉到 下面關(guān)注公眾號(hào)也可以吧 【Vue原理】Compile - 源碼版 之 Parse 主要流程 本文難度較繁瑣,需要耐心觀看,如果你對(duì) compile 源碼暫時(shí)...
摘要:圖在中應(yīng)用三數(shù)據(jù)渲染過程數(shù)據(jù)綁定實(shí)現(xiàn)邏輯本節(jié)正式分析從到數(shù)據(jù)渲染到頁面的過程,在中定義了一個(gè)的構(gòu)造函數(shù)。一、概述 vue已是目前國內(nèi)前端web端三分天下之一,也是工作中主要技術(shù)棧之一。在日常使用中知其然也好奇著所以然,因此嘗試閱讀vue源碼并進(jìn)行總結(jié)。本文旨在梳理初始化頁面時(shí)data中的數(shù)據(jù)是如何渲染到頁面上的。本文將帶著這個(gè)疑問一點(diǎn)點(diǎn)追究vue的思路??傮w來說vue模版渲染大致流程如圖1所...
摘要:一旦我們檢測(cè)到這些子樹,我們可以把它們變成常數(shù),這樣我們就不需要了在每次重新渲染時(shí)為它們創(chuàng)建新的節(jié)點(diǎn)在修補(bǔ)過程中完全跳過它們。否則,吊裝費(fèi)用將會(huì)增加好處大于好處,最好總是保持新鮮。 寫文章不容易,點(diǎn)個(gè)贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內(nèi)部詳情,讓我們一起學(xué)習(xí)吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,...
摘要:今年的月日,的版本正式發(fā)布了,其中核心代碼都進(jìn)行了重寫,于是就專門花時(shí)間,對(duì)的源碼進(jìn)行了學(xué)習(xí)。本篇文章就是源碼學(xué)習(xí)的總結(jié)。實(shí)現(xiàn)了并且將靜態(tài)子樹進(jìn)行了提取,減少界面重繪時(shí)的對(duì)比。的最新源碼可以去獲得。 Vue2.0介紹 從去年9月份了解到Vue后,就被他簡潔的API所吸引。1.0版本正式發(fā)布后,就在業(yè)務(wù)中開始使用,將原先jQuery的功能逐步的進(jìn)行遷移。 今年的10月1日,Vue的2...
閱讀 2421·2021-11-25 09:43
閱讀 1255·2021-11-24 09:39
閱讀 756·2021-11-23 09:51
閱讀 2391·2021-09-07 10:18
閱讀 1881·2021-09-01 11:39
閱讀 2784·2019-08-30 15:52
閱讀 2599·2019-08-30 14:21
閱讀 2865·2019-08-29 16:57