寫文章不容易,點個讚唄兄弟
專一 Vue 源碼分享,文章分爲白話版和 源碼版,白話版助於理解工做原理,源碼版助於瞭解內部詳情,讓咱們一塊兒學習吧 研究基於 Vue版本 【2.5.17】html
若是你以爲排版難看,請點擊 下面連接 或者 拉到 下面關注公衆號也能夠吧express
【Vue原理】Compile - 源碼版 之 Parse 主要流程 數組
本文難度較繁瑣,須要耐心觀看,若是你對 compile 源碼暫時不感興趣能夠先移步白話版 Compile - 白話版,緩存
parse 是 渲染三巨頭的老大,其做用是把 template 字符串模板,轉換成 astbash
其涉及源碼也是多得一批,達到了 一千多行,想一想若是我把所有源碼放到文章裏面來簡直不能看,因此我打算只保留主要部分,就是正常流程能夠走通,去掉那些特殊處理的地方函數
大部分源碼都是特殊處理,好比 script ,style,input ,pre 等標籤,此次所有都去掉,只留下通用元素的處理流程,留下一個骨架學習
由於 parse 的內容很是的多,除了精簡源碼以外,我還經過不一樣內容劃分文章去記錄優化
今天,要記錄的就是 parse 解析 template 成 ast 的大體流程,而怎麼解析標籤名,怎麼解析標籤屬性會暫時忽略,而獨立成文。當有解析標籤名和解析屬性的地方會直接出結果。好比當我說在 模板 "<div></div>" 匹配出頭標籤時,直接就獲得 div ,而不會去考究是如何匹配出來的ui
好的,到底 template 是怎麼變成 ast 的呢?跟着我去探索把~spa
先來講說 ast 吧,這種複雜的概念,反正是須要查的。因此本文根本不須要解釋太多
直接說個人理解吧
抽象語法樹,以樹狀形式表現出語法結構
直接使用例子去直觀感覺就行了
<div>111</div>
複製代碼
用 ast 去描述這個模板就是
{
tag:'div',
type :1 ,
children:[ {
type:3,
text:'11'
} ]
}
複製代碼
簡單得一批把,複雜的這裏也不提了,反正跟 parse 沒多大關係我以爲
另外記一下,節點的 type 表示的意思
type:1,節點
type:2,表達式,好比 {{isShow}}
type:3,純文本
如今就開始 parse 的內容了,那麼就看 parse 的源碼
parse 是渲染三巨頭的老大,同時它也是一個函數,源碼以下
function parse(template) {
var stack = []; // 緩存模板中解析的每一個節點的 ast
var root; // 根節點,是 ast
var currentParent; // 當前解析的標籤的父節點
/**
* parseHTML 處理 template 匹配標籤,再傳入 start,end,chars 等方法
**/
parseHTML(template, {
start: (..被抽出,在後面)
end: (..被抽出,在後面), // 爲 起始標籤 開啓閉合節點
chars: (..被抽出,在後面) // 文字節點
});
return root
}
複製代碼
parse 接收 template 字符串,使用 parseHTML 這個函數在 template 中匹配標籤
並傳入 start,end,chars 三個函數 供 parseHTML 處理標籤等內容
start,end,chars 方法都已經被我抽出來,放在後面逐個說明
下面來看下其中聲明的三個變量
是一個數組存放模板中按順序 從頭至尾 每一個標籤的 ast
注:不會存放單標籤的 ast ,好比 input,img 這些
好比 stack 是這樣的
stack=[{
tag:'div',
type :1 ,
children:[ {
type:3,
text:'11'
} ]
}]
複製代碼
主要做用是幫助理清節點父子關係
每一個模板都必須有一個根節點。寫過 Vue 項目的都知道了,因此通常解析到第一個標籤的時候,會直接設置這個標籤爲 根節點
而且最後返回的也是 root
不能夠存在兩個根節點(有 v-if 的不討論)
在解析標籤的時候,必需要知道這個標籤的 父節點時誰
這樣才知道 這個標籤是誰的子節點,才能把這個節點添加給相應的 節點的 children
注:根節點 沒有 父節點,因此就是 undefined
parse 源碼已經被我精簡得很簡單了,主要內容其實就在 其中涉及的四個方法中
parseHTML,start,end,chars
parseHTML 是處理 template 的主力,其餘三個函數是功能類型的,負責處理相應的內容。 例如,start 是處理頭標籤的,end 是處理尾標籤的,chars 是處理文本的
先來看看 parseHTML
parseHTML 做爲處理 template,匹配標籤的函數,是十分龐大的,其中兼顧了很是多狀況的處理
而本次在不影響流程的狀況下,我去掉了下面這些處理,優化閱讀
一、沒有結束標籤的處理
二、文字中包含 < 的處理
三、註釋的處理
四、忽略首尾空白字符,默認起始和結尾都是標籤
我的認爲主要內容爲三個
一、循環 template 匹配標籤
二、把匹配到的內容,傳給相應的方法處理
三、截斷 template
來看源碼,已經簡化得不行了,可是仍是要花點心思看看
function parseHTML(html, options) {
while (html) {
// 尋找 < 的起始位置
var textEnd = html.indexOf('<'),
text ,rest ,next;
// 模板起始位置是標籤開頭 <
if (textEnd === 0) {
/**
* 若是是尾標籤的 <
* 好比 html = '</div>' , 匹配出 endTagMatch =["</div>", "div"]
*/
var endTagMatch = html.match(endTag);
if (endTagMatch) {
// endTagMatch[0]="</a>"
html = html.substring(endTagMatch[0].length);
// 處理尾標籤,方法後面有記錄
options.end();
continue
}
/**
* 若是是起始標籤的 <
* parseStartTag 做用是,匹配標籤存在的屬性,截斷 template
* html = '<div></div>',
* parseStartTag 處理以後,startTagMatch = {tagName: "div", attrs: []}
*/
var startTagMatch = parseStartTag();
// 匹配到 起始標籤以後
if (startTagMatch) {
// 處理起始標籤,後面有介紹
options.start(起始標籤的信息);
continue
}
}
// 模板起始位置不是 <,而是文字
if (textEnd >= 0) {
text = html.substring(0, textEnd);
html = html.substring(n);
}
// 處理文字,後面有介紹
if (options.chars && text) {
options.chars(text);
}
}
}
複製代碼
思路以下
由於他是標籤的開頭(已經排除了文字中含有 < 的處理,不作討論)
那麼多是 尾標籤,多是 頭標籤,那麼就須要判斷究竟是哪一個
一、先匹配尾標籤,若是匹配到,那麼就是尾標籤,使用 end 方法處理。
二、若是不是,使用 parseStartTag 函數匹配獲得首標籤,並把 首標籤信息傳給 start 處理
parseStartTag 就是使用正則在template 中匹配出 首標籤信息,其中包括標籤名,屬性等
好比 template 是
html = '<div name="22">111</div>;'
複製代碼
parseStartTag 處理匹配以後獲得
{
tagName: "div",
attrs: [{name:"22"}]
}
複製代碼
那麼證實 開頭 到 < 的位置這一段,是字符串,那麼就是文本了
傳給 chars 方法處理
每次處理一次,就會截斷到匹配的位置,而後 template 愈來愈短,直接爲空,退出 while,因而處理完畢
對於截斷呢,使用 substring,可能忘了怎麼做用的,寫個小例子
傳入數字,表示這個位置前面的字符串都不要
而後,就到了咱們其餘三個方法的閃亮登場了
每當 parseHTML 匹配到一個 首標籤,都會把該標籤的信息傳給 start 方法,讓他來處理
function start(tag, attrs, unary) {
// 建立 AST 節點
var element = createASTElement(tag, attrs, currentParent);
/**
* ...省略了一段處理 vFor,vIf,解析 @ 等屬性指令的代碼
**/
// 設置根節點,一個模板只有一個根節點
if (!root) root = element;
// 處理父子關係
if (currentParent) {
currentParent.children.push(element);
element.parent = currentParent;
}
// 不是單標籤(input,img 那些),就須要保存 stack
if (!unary) {
currentParent = element;
stack.push(element);
}
}
複製代碼
精簡得一目瞭然(面目全非),看得極度溫馨
看看 start 方法都作了哪些惡呢
一、建立 ast
二、解析 attrs,並存放到 ast (已省略屬性解析)
三、設置根節點,父節點,把節點添加進父節點的 children
四、ast 保存進 stack
好像不用解釋太多,確定都看得懂啊,除了一個 建立 ast 的函數
這就來源碼
function createASTElement(tag, attrs, parent) {
return {
type: 1,
tag: tag,
attrsList: attrs,
// 把 attrs 數組 轉成 對象
attrsMap: makeAttrsMap(attrs),
parent: parent,
children: []
}
}
複製代碼
建立一個 ast 結構,保存數據
直接返回一個對象,很是明瞭,包含的各類屬性,應該也能看懂
其中有一個 makeAttrsMap 函數,舉個栗子
模板上的屬性,通過 parseHTML 解析成一個數組,以下
[{
name:"hoho" ,value:"333"
},{
name:"href" ,value:"444"
}]
複製代碼
makeAttrMap 轉成對象成這樣
{ hoho:"333", href:"444"}
複製代碼
而後就保存在 ast 中
每當 parseHTML 匹配到 尾標籤 ,好比 "" 的時候,就會調用傳入的 end 方法
來看看吧
function end() {
// 標籤解析結束,移除該標籤
stack.length -= 1;
currentParent = stack[stack.length - 1];
}
複製代碼
乍一看,很簡單啊!這麼少(都是精簡...)
做用有兩個
stack 保存的是匹配到的頭標籤,若是標籤已經匹配結束了,那麼就須要移除
stack 就是爲了明確各節點間父子關係而存在的
保證 stack 中最後一個節點,永遠是下次匹配的節點的父節點
舉個栗子,存在下面模板
stack 匹配兩個 頭標籤以後
stack = [ 'div' , 'section']
複製代碼
看看 start 能夠知道,此時 currentParent = section
而後匹配到 ,則移除 stack 中的 section,而且重設 currentParent
stack = ['div']
currentParent = 'div'
複製代碼
再匹配到 p 的時候,p 的父節點就是 div,父子順序就是正確的了
當 parseHTML 去匹配 < 的時候,發現 template 不是 <,template開頭 到 < 還有一段距離
那麼這段距離的內容就是 文本了,那麼就會把這段文本傳給 chars 方法處理
來看看源碼
function chars(text) {
// 必須存在根節點,不可能用文字開頭
if(!currentParent) return
var children = currentParent.children;
// 經過 parseText 解析成字符串,判斷是否含有雙括號表達式,好比 {{item}}
// 若是是有表達式,會存放多一些信息,
var res = parseText(text)
if(res) {
children.push({
type: 2,
expression: res.expression,
tokens: res.tokens,
text: text
});
}
// 普通字符串,直接存爲 字符串子節點
else if(
!children.length ||
children[children.length - 1].text !== ' '
) {
children.push({
type: 3,
text: text
});
}
}
複製代碼
這段代碼主要做用就是,爲 父節點 添加 文本子節點
而文本子節點分爲兩種類型
一、普通型,直接存爲文本子節點
二、表達式型,須要通過 parseText 處理
直接以結果來定義吧
好比處理這段文本
{{isShow}}
{
expression: toString(isShow)
tokens: [{@binding: "isShow"}]
}
複製代碼
主要是爲了把表達式 isShow 拿到,方便後面從實例上獲取值
好的,如今,template 處理流程所涉及的主要方法都講完了
如今用上面這些函數來走一個流程
如今有一個模板
<div>11 </div>
複製代碼
匹配到第一個 頭標籤 (
該 div 的 ast 變成根節點 root,並設置其爲當前父節點 currentParent,保存進節點緩存數組 stack
此時
stack = [ { tag:'div' , children:[ ] } ]
複製代碼
第一輪處理結束,template 截斷到第一次匹配到的位置
此時,template = 11
開始匹配 <,發現 < 不在開頭,而 開頭位置 到 < 有一段普通字符串
調用 parse-char,傳入字符串
發現其沒有 雙括號表達式,直接給父節點添加簡單子節點
currentParent.children.push({ type:3 , text:'11' })
複製代碼
此時
stack =[ { tag:'div' , children:[ { type:3 , text:'11' } ] } ]
複製代碼
第二輪處理結束,template 截斷到剛剛匹配完的字符串
此時,template =
繼續尋找 <,發現就在開頭,可是這是一個結束標籤,標籤名是 div
由於 stack 是節點順序存入的,這個結束標籤確定屬於 stack 最後一個 標籤
因爲 該標籤匹配完畢,因此從 stack 中移除
而且設置 當前父節點 currentParent 爲 stack 倒數第二個
第三次遍歷結束,template 繼續截斷
此時 template 爲空了,結束全部遍歷
返回這次 tempalte 解析的 root
{
tag:'div',type :1 ,
children:[ { type:3 , text:'11' } ]
}
複製代碼
因而 parse 就成功把 tempalte 解析成了 ast ,就是 root
本問講的是 parse 的主要流程,忽略了內部的處理細節,好比怎麼解析標籤,怎麼解析屬性,其餘內容都會獨立成文章
在 parse 的流程中,大體有五個函數,咱們屢一下,以下
parse,parseHTML,start,end,chars
parse 是整個 parse 流程的總函數
parseHTML 是 parse 處理的主力函數
start,end,chars 是 在 parse 中傳給 parseHTML ,用來幫助處理 匹配的標籤信息的函數,這三個函數會在 parseHTML 中被調用
鑑於本人能力有限,不免會有疏漏錯誤的地方,請你們多多包涵,若是有任何描述不當的地方,歡迎後臺聯繫本人,有重謝