摘要:注意看注釋很粗很簡單,我就是一程序員姓名,年齡,請聯系我吧是否保留注釋定義分隔符,默認為對于轉成,則需要先獲取,對于這部分內容,做一個簡單的分析,具體的請自行查看源碼。其中的負責修改以及截取剩余模板字符串。
通過查看vue源碼,可以知道Vue源碼中使用了虛擬DOM(Virtual Dom),虛擬DOM構建經歷 template編譯成AST語法樹 -> 再轉換為render函數 最終返回一個VNode(VNode就是Vue的虛擬DOM節點) 。
本文通過對Vue源碼中的AST轉化部分進行簡單提取,返回靜態的AST結構(不考慮兼容性及屬性的具體解析)。并最終根據一個實例的template轉化為最終的AST結構。
在Vue的mount過程中,template會被編譯成AST語法樹,AST是指抽象語法樹(abstract syntax tree或者縮寫為AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式。
代碼分析首先、定義一個簡單的html DOM結構、其中包括比較常見的標簽、文本以及注釋,用來生成AST結構。
很粗
很簡單,我就是一程序員
姓名:{{name}},年齡:{{age}}, 請聯系我吧
對于轉成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: "", if (typeof template === "string") { // 如果為"#template" if (template.charAt(0) === "#") { let tpl = query(template); template = tpl ? tpl.innerHTML : ""; } // 否則不做處理,如:"" } else if (template.nodeType) { // 如果模板為DOM節點,如:template: document.querySelector("#template") // 比如: 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 = /^"); if (commentEnd >= 0) { if (opt.shouldKeepComment && opt.comment) { // 保存注釋內容 opt.comment(html.substring(4, commentEnd)) } // 調整index以及html advance(commentEnd + 3); continue; } } // 處理 html條件注釋, 如 // 處理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("([sS]*?)(" + 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); // 處理結束標簽// 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) { // 結構:[" // 調整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等 // 形如: // 與parseEndTag中tagName為p時相對應,處理,添加// 處理結果:
parseEndTag(lastTag); } if (canBeLeftOpenTag(tagName) && lastTag === tagName) { // 如果標簽閉合標簽可以省略"/" // 形如:
將開始標簽處理成特定結構后,再通過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") { // 處理: if (opt.start) { opt.start(tagName, [], true) } } else if (tagName === "p") { // 處理上面說的情況: if (opt.start) { opt.start(tagName, [], false); } if (opt.end) { opt.end(); } } }
parseEndTag中,處理結束標簽時,需要一層層往外,在stack中找到當前標簽最近的相同標簽,獲取stack中的位置,如果標簽匹配正確,一般為stack中的最后一個(否則缺少結束標簽),如果匹配成功,將棧中的匹配標簽出棧,并重新設置lastTag為棧中的最后一個。
注意:需要特殊處理br或p標簽,標簽在stack中找不到對應的匹配標簽,需要多帶帶保存到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 + "((?:.| )+?)" + 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 } }
最后,附上以上原理簡略分析圖:
解析流程如下: 分析過程: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]很粗
很簡單,我就是一程序員
姓名:{{name}},年齡:{{age}}, 請聯系我吧
最終AST結構如下:
以上是我根據vue源碼分析,抽出來的簡單的template轉化AST,文中若有什么不對的地方請大家幫忙指正,本人最近也一直在學習Vue的源碼,希望能夠拿出來與大家一起分享經驗,接下來會繼續更新后續的源碼,如果覺得有需要可以相互交流。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/95814.html
直接進入核心現在說說baseCompile核心代碼: //`createCompilerCreator`allowscreatingcompilersthatusealternative //parser/optimizer/codegen,e.gtheSSRoptimizingcompiler. //Herewejustexportadefaultcompilerusingthede...
寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請點擊 下面鏈接 或者 拉到 下面關注公眾號也可以吧 【Vue原理】Compile - 源碼版 之 Parse 主要流程 本文難度較繁瑣,需要耐心觀看,如果你對 compile 源碼暫時...
摘要:圖在中應用三數據渲染過程數據綁定實現邏輯本節正式分析從到數據渲染到頁面的過程,在中定義了一個的構造函數。一、概述 vue已是目前國內前端web端三分天下之一,也是工作中主要技術棧之一。在日常使用中知其然也好奇著所以然,因此嘗試閱讀vue源碼并進行總結。本文旨在梳理初始化頁面時data中的數據是如何渲染到頁面上的。本文將帶著這個疑問一點點追究vue的思路。總體來說vue模版渲染大致流程如圖1所...
摘要:一旦我們檢測到這些子樹,我們可以把它們變成常數,這樣我們就不需要了在每次重新渲染時為它們創建新的節點在修補過程中完全跳過它們。否則,吊裝費用將會增加好處大于好處,最好總是保持新鮮。 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,...
摘要:今年的月日,的版本正式發布了,其中核心代碼都進行了重寫,于是就專門花時間,對的源碼進行了學習。本篇文章就是源碼學習的總結。實現了并且將靜態子樹進行了提取,減少界面重繪時的對比。的最新源碼可以去獲得。 Vue2.0介紹 從去年9月份了解到Vue后,就被他簡潔的API所吸引。1.0版本正式發布后,就在業務中開始使用,將原先jQuery的功能逐步的進行遷移。 今年的10月1日,Vue的2...
閱讀 2417·2021-11-25 09:43
閱讀 1250·2021-11-24 09:39
閱讀 752·2021-11-23 09:51
閱讀 2389·2021-09-07 10:18
閱讀 1867·2021-09-01 11:39
閱讀 2783·2019-08-30 15:52
閱讀 2598·2019-08-30 14:21
閱讀 2863·2019-08-29 16:57