寫文章不容易,點個讚唄兄弟
專一 Vue 源碼分享,文章分爲白話版和 源碼版,白話版助於理解工做原理,源碼版助於瞭解內部詳情,讓咱們一塊兒學習吧 研究基於 Vue版本 【2.5.17】html
若是你以爲排版難看,請點擊 下面連接 或者 拉到 下面關注公衆號也能夠吧數組
【Vue原理】Compile - 源碼版 之 標籤解析 bash
咳咳,上一篇文章,咱們已經大體把 parse 的流程給記錄了一遍,若是沒看過,比較建議,先把這個流程給看了 Compile - 源碼版 之 Parse 主要流程 函數
可是忽略了其中的處理細節,好比標籤怎麼解析的,屬性怎麼解析的,並且這兩個內容也是很是多的,因此須要單獨拎出來詳細記錄,否則混在一塊兒,又臭又長學習
白話版在這~ Compile - 白話版 ui
今天的內容是,記錄 標籤解析的 源碼spa
首先,開篇以前呢,咱們來了解一下文章會出現過的正則3d
var ncname = '[a-zA-Z_][\\w\\-\\.]*';
var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
var startTagOpen = new RegExp(("^<" + qnameCapture));
var startTagClose = /^\s*(\/?)>/;
var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
複製代碼
主要是四個rest
匹配 頭標籤的 前半部分。當字符串開頭是 頭標籤時,能夠匹配code
匹配 頭標籤的 右尖括號。當字符串開頭是 > 時,能夠匹配
匹配 尾標籤。當字符串開頭是 尾標籤時 能夠匹配
匹配標籤上的屬性。當字符串開頭是屬性則能夠匹配
好的,看完上面四個正則, 內心有個 * 數以後,相信下面的內容你會更加清晰些
下面的內容分爲
一、循環遍歷 template
二、處理 頭標籤
三、處理 尾標籤
那麼咱們按一個個來講
經過上一篇內容,已經記錄過 是怎麼循環遍歷 template 的了,就是經過 parseHTML 這個方法
這個方法,由於內容須要,也記錄一遍
首先,什麼是循環遍歷template?
template 是一個字符串,因此每匹配完一個信息(好比頭標籤等),就會把template 截斷到匹配的結束位置
好比 template 是
"<div>1111</div>"
複製代碼
當咱們匹配完了 頭標籤,那麼 template 就會被截斷成
"1111</div>"
複製代碼
而後就這樣一直循環匹配新的 template,直到 template 被截斷成 空字符串,那麼匹配完畢,其中跟截斷有關的一個重要函數就是 advance
這個函數在下面的源碼中用得很是多,須要牢記
其做用就是
一、截斷template
二、保存當前截斷的位置。好比你匹配了template到 字符串長度爲5 的位置,那麼 index 就是 4(從0開始)
function advance(n) {
index += n;
html = html.substring(n);
}
複製代碼
記住這個函數哦,我傳入一個數字 n,就是要把 template 從 n 截取到結尾
而後下面就看看簡化的 parseHTML 源碼(若是嫌長,先跳到分析)
function parseHTML(html, options) {
// 保存全部標籤的對象信息,tagName,attr,這樣,在解析尾部標籤的時候獲得所屬的層級關係以及父標籤
var stack = [];
var index = 0;
var last;
while (html) {
last = html;
var textEnd = html.indexOf('<');
// 若是開頭是 標籤的 <
if (textEnd === 0) {
/**
* 若是開頭的 < 屬性尾標籤
* 好比 html = '</div>'
* 匹配出 endTagMatch =["</div>", "div"]
*/
* 若是開頭的 < 屬性尾標籤
* 好比 html = '</div>'
* 匹配出 endTagMatch =["</div>", "div"]
*/
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
// endTagMatch[0]="</div>"
advance(endTagMatch[0].length);
// endTagMatch[1]="div"
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
/**
* 若是開頭的 < 屬性 頭標籤
* parseStartTag 做用是,匹配標籤存在的屬性,截斷 template
* html = '<div></div>'
* startTagMatch = {tagName: "div", attrs: []}
*/
* 若是開頭的 < 屬性 頭標籤
* parseStartTag 做用是,匹配標籤存在的屬性,截斷 template
* html = '<div></div>'
* startTagMatch = {tagName: "div", attrs: []}
*/
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
}
}
var text ,rest ,next ;
// 模板起始位置 不是 <,而是文字
if (textEnd >= 0) {
text = html.substring(0, textEnd);
advance(textEnd);
}
// 處理文字,上篇文章已經講過
if (options.chars && text) {
options.chars(text);
}
}
function parseStartTag(){...}
function handleStartTag(){...}
function parseEndTag(){...}
}
複製代碼
這段代碼已經簡化得很簡單了,算是總體對 template 處理的一種把控我以爲
先匹配 < 的位置
那麼就是標籤(這裏先不討論 字符串中的 <)
而後須要多一層判斷
若是是尾標籤的 <,那麼交給 parseEndTag 處理
若是是頭標籤的 <,那麼使用 handleStartTag 處理
那麼代表 開頭到 < 的這段位置是字符串,可是本文內容是標籤解析,因此忽略這部分
而後每完成一次匹配,就須要調用 advace 去截斷 template
而後如今,咱們假定有下面這段處理
template = "<div>111</div>"
parseHTML(template)
複製代碼
匹配 < 在開頭,正則判斷以後,發現不是 尾標籤的 <,那麼須要判斷是否是 頭標籤的
而後使用 parseStartTag 方法去匹配頭標籤信息
匹配成功,使用 handleStartTag 方法處理
看到在 parseHTML 末尾聲明瞭三個函數,爲了不太長,我挑了出來放在相應的內容講
而之因此會在裏面聲明這個三個函數,是爲了在這三個函數中,能訪問到 parseHTML 中的變量,好比 stack,index
這個方法的做用就是
一、把頭標籤的全部信息集合起來,包括屬性,標籤名等
二、匹配完成以後一樣調用 advance 去截斷 template
三、把標籤信息 返回
源碼已經簡化,而且有作流程註釋,你們確定看得懂,太煩的能夠看後面的結果
function parseStartTag() {
// html ='<div name=1>111</div>'
// start = ["<div", "div", index: 0]
var start = html.match(startTagOpen);
if (start) {
// 存儲本次頭標籤的信息
var match = {
tagName: start[1],
attrs: [],
start: index
};
// start[0] 是 <div
// 截斷以後,template = "name=1 >111</div>"
advance(start[0].length);
var end, attr;
// 循環匹配 屬性 內容,保存屬性列表
// 直到 template 開頭是 頭標籤的 >
while (
// 匹配不到頭標籤的 >,開始匹配 屬性內容
// end = null
! (end = html.match(startTagClose))
&&
// 開始匹配 屬性內容
// attr = ["name=1", "name", "=" ]
(attr = html.match(attribute))
) {
advance(attr[0].length);
match.attrs.push(attr);
}
// 匹配到 起始標籤的 >,標籤屬性那些已經匹配完畢了
// 返回收集到的 標籤信息
if (end) {
advance(end[0].length);
// 若是是單標籤,那麼 unarySlash 的值是 /,好比 <input />
match.unarySlash = end[1];
match.end = index;
return match
}
}
}
複製代碼
咱們來記錄下這個方法會返回什麼
好比
html = "<div name=1></div>"
複製代碼
parseStartTag 處理以後會返回如下內容
{
tagName: "div",
attrs: [
[" name=1", "name", "=" ,
undefined, undefined, "1"]
],
unarySlash: "",
start: 0,
end: 12
}
複製代碼
其中 的屬性
start:頭標籤的 < 在 template 中的位置
end:頭標籤的 > 在 tempalte 中的位置
attrs:是一個二維數組,存放着全部頭標籤的 屬性信息
unarySlash:表示這個標籤是不是 單標籤。若是是 true,那麼不是單標籤。若是是 false,那麼就是單標籤。一切在於匹配頭標籤時,有沒有匹配到 /
經過 parseHTML 咱們看到,parseStartTag 返回的 頭標籤信息,給了誰呢?
沒錯,傳給了 handleStartTag
這個函數的做用
一、接收上一步收集的標籤信息
二、處理屬性,轉換一下其格式
三、保存進 stack,記錄 DOM 父子結構順序
function handleStartTag(match) {
var tagName = match.tagName;
var unarySlash = match.unarySlash;
// 判斷是否是單標籤,input,img 這些
var unary = isUnaryTag$$1(tagName) || !!unarySlash;
var l = match.attrs.length;
var attrs = new Array(l);
// 把屬性數組轉換成對象
for (var i = 0; i < l; i++) {
var args = match.attrs[i];
// args = [" name=1", "name", "=",
undefined, undefined, "1" ]
var value = args[3] || args[4] || args[5] || '';
attrs[i] = {
name: args[1],
value: value
};
}
// 不是單標籤,才存到 stack
if (!unary) {
stack.push({
tag: tagName,
attrs: attrs
});
}
if (options.start) {
options.start(
tagName, attrs, unary,
match.start, match.end
);
}
}
複製代碼
最後,把該標籤獲得的信息,傳給 options.start,幫助創建 template 的 ast(在 上篇文章 Compile - 源碼版 之 Parse 主要流程 中有說明=)
那麼到這裏,頭標籤 匹配完了
而後 template 被截斷成
"111</div>"
複製代碼
文本處理的部分咱們跳過,跳到尾標籤,因此 template 爲
"</div>"
複製代碼
而後匹配到尾標籤,交給 parseEndTag 處理
那麼進入咱們的下一小節內容,處理尾標籤
在 parseHTML 中看到
當使用 endTag 這個正則成功匹配到尾標籤時,會調用 parseEndTag
而 這個函數呢,可能沒有那麼好理解了,你能夠先跳過源碼,翻到後面的解析
function parseEndTag(tagName, start, end) {
var pos, lowerCasedTagName;
// 從stack 最後查找匹配的 tagName 位置
if (tagName) {
// 若是在 stack 中找不到,pos 最後是 -1
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].tagName===tagName) break
}
}
else {
// 若是沒有提供標籤名,那麼關閉全部存在 stack 中的 起始標籤
pos = 0
}
// 批量 stack pos 位置後的全部標籤
if (pos >= 0) {
// 關閉 pos 位置以後全部的起始標籤,避免有些標籤沒有尾標籤
// 好比 stack.len = 7 , pos=5 ,那麼就關閉 最後兩個
for (var i = stack.length - 1; i >= pos; i--) {
if (options.end) {
options.end(stack[i].tag, start, end);
}
}
// 匹配完閉合標籤以後,就把 匹配了的標籤頭 給 移除了
stack.length = pos;
}
}
複製代碼
函數功能分爲兩部分
一、找位置。從 stack 結尾,找到 tagName 所在位置 pos
二、批量閉合。閉合並移除 stack 在 pos 位置後的全部 tag
如今咱們先給一個模板,而後慢慢解釋
<div>
<header>
<span></span>
</header>
</div>
複製代碼
如今已經連續匹配到三個 頭標籤,div,header,span
此時 stack= [ div, header, span ]
而後開始匹配到 ,而後去 stack 末尾找 span
肯定 span 在 stack 的位置 pos 後,批量閉合stack 的 pos 後的全部標籤
由於 stack 是按 template 的標籤順序存放的,確定是先匹配到父標籤,再匹配到子標籤
碰到 尾標籤,確定找最近匹配到的頭標籤,那麼確定是剛存入 stack 的,那麼就是在 stack 的結尾
由於怕有刁民不寫閉合標籤,好比模板是這樣
<div>
<header>
<span>
</header>
</div>
複製代碼
一樣,匹配完三個頭標籤
stack = [ div, header, span ]
複製代碼
接着匹配到 ,因而在 stack 末尾中找 header
在倒數第二個,那麼 pos = 1
根據 stack 的長度, 遍歷一次 stack,閉合到末尾,就是閉合 stack[1], stack[2]
就是閉合 header 和 span 了
就是爲了不有屁民沒寫 尾標籤
是啊,可是單標籤 不存進 stack 啊哈哈哈
在 handleStartTag 中有處理哦
接下來你就能夠去看 parseEndTag 的源碼了,確定能看懂
parseEndTag 就作了匹配 tag 位置 和容錯處理
主要實現閉合功能在調用 options.end 中,經過不斷地傳入尾標籤從而完成閉合功能
若是你看過上篇文章 Compile - 源碼版 之 Parse 主要流程 就知道,閉合是爲了造成正確的節點關係樹
也能夠說,是爲了明確節點的父子關係
經過此次咱們知道了
一、標籤的匹配方法
二、怎麼利用頭標籤收集信息
三、閉合標籤的處理方法
鑑於本人能力有限,不免會有疏漏錯誤的地方,請你們多多包涵,若是有任何描述不當的地方,歡迎後臺聯繫本人,有重謝