寫文章不容易,點個贊唄兄弟
專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧
研究基于 Vue版本 【2.5.17】
如果你覺得排版難看,請點擊 下面鏈接 或者 拉到 下面關注公眾號也可以吧
【Vue原理】Compile - 源碼版 之 Parse 主要流程
本文難度較繁瑣,需要耐心觀看,如果你對 compile 源碼暫時不感興趣可以先移步白話版 Compile - 白話版,
parse 是 渲染三巨頭的老大,其作用是把 template 字符串模板,轉換成 ast
其涉及源碼也是多得一批,達到了 一千多行,想想如果我把全部源碼放到文章里面來簡直不能看,所以我打算只保留主要部分,就是正常流程可以走通,去掉那些特殊處理的地方
大部分源碼都是特殊處理,比如 script ,style,input ,pre 等標簽,這次全部都去掉,只留下通用元素的處理流程,留下一個骨架
因為 parse 的內容非常的多,除了精簡源碼之外,我還通過不同內容劃分文章去記錄
今天,要記錄的就是 parse 解析 template 成 ast 的大致流程,而怎么解析標簽名,怎么解析標簽屬性會暫時忽略,而獨立成文。當有解析標簽名和解析屬性的地方會直接出結果。比如當我說在 模板 "
" 匹配出頭標簽時,直接就得到 div ,而不會去考究是如何匹配出來的好的,到底 template 是怎么變成 ast 的呢?跟著我去探索把~
AST先來說說 ast 吧,這種復雜的概念,反正是需要查的。所以本文根本不需要解釋太多
直接說我的理解吧
抽象語法樹,以樹狀形式表現出語法結構
直接使用例子去直觀感受就好了
111
用 ast 去描述這個模板就是
{ tag:"div", type :1 , children:[ { type:3, text:"11" } ] }
簡單得一批把,復雜的這里也不提了,反正跟 parse 沒多大關系我覺得
另外記一下,節點的 type 表示的意思
type:1,節點
type:2,表達式,比如 {{isShow}}
type:3,純文本
現在就開始 parse 的內容了,那么就看 parse 的源碼
Parseparse 是渲染三巨頭的老大,同時它也是一個函數,源碼如下
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 方法都已經被我抽出來,放在后面逐個說明
下面來看下其中聲明的三個變量
1 stack是一個數組存放模板中按順序 從頭到尾 每個標簽的 ast
注:不會存放單標簽的 ast ,比如 input,img 這些
比如 stack 是這樣的
stack=[{ tag:"div", type :1 , children:[ { type:3, text:"11" } ] }]
主要作用是幫助理清節點父子關系
2 root每個模板都必須有一個根節點。寫過 Vue 項目的都知道了,所以一般解析到第一個標簽的時候,會直接設置這個標簽為 根節點
并且最后返回的也是 root
不可以存在兩個根節點(有 v-if 的不討論)
3 currentParent在解析標簽的時候,必須要知道這個標簽的 父節點時誰
這樣才知道 這個標簽是誰的子節點,才能把這個節點添加給相應的 節點的 children
注:根節點 沒有 父節點,所以就是 undefined
parse 源碼已經被我精簡得很簡單了,主要內容其實就在 其中涉及的四個方法中
parseHTML,start,end,chars
parseHTML 是處理 template 的主力,其他三個函數是功能類型的,負責處理相應的內容。 例如,start 是處理頭標簽的,end 是處理尾標簽的,chars 是處理文本的
先來看看 parseHTML
處理 templateparseHTML 作為處理 template,匹配標簽的函數,是十分龐大的,其中兼顧了非常多情況的處理
而本次在不影響流程的情況下,我去掉了下面這些處理,優化閱讀
1、沒有結束標簽的處理
2、文字中包含 < 的處理
3、注釋的處理
4、忽略首尾空白字符,默認起始和結尾都是標簽
個人認為主要內容為三個
1、循環 template 匹配標簽
2、把匹配到的內容,傳給相應的方法處理
3、截斷 template
來看源碼,已經簡化得不行了,但是還是要花點心思看看
function parseHTML(html, options) { while (html) { // 尋找 < 的起始位置 var textEnd = html.indexOf("<"), text ,rest ,next; // 模板起始位置是標簽開頭 < if (textEnd === 0) { /** * 如果是尾標簽的 < * 比如 html = "
思路如下
1匹配 < 這個符號因為他是標簽的開頭(已經排除了文字中含有 < 的處理,不做討論)
2如果 template 開頭是 <那么可能是 尾標簽,可能是 頭標簽,那么就需要判斷到底是哪個
1、先匹配尾標簽,如果匹配到,那么就是尾標簽,使用 end 方法處理。
2、如果不是,使用 parseStartTag 函數匹配得到首標簽,并把 首標簽信息傳給 start 處理
parseStartTag 就是使用正則在template 中匹配出 首標簽信息,其中包括標簽名,屬性等
比如 template 是
html = "111;"
parseStartTag 處理匹配之后得到
{ tagName: "div", attrs: [{name:"22"}] }3 如果 template 開頭不是 <
那么證明 開頭 到 < 的位置這一段,是字符串,那么就是文本了
傳給 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 方法都做了哪些惡呢
1、創建 ast
2、解析 attrs,并存放到 ast (已省略屬性解析)
3、設置根節點,父節點,把節點添加進父節點的 children
4、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 匹配到 尾標簽 ,比如 "
來看看吧
function end() { // 標簽解析結束,移除該標簽 stack.length -= 1; currentParent = stack[stack.length - 1]; }
乍一看,很簡單??!這么少(都是精簡...)
作用有兩個
1從 stack 數組中移除這個節點stack 保存的是匹配到的頭標簽,如果標簽已經匹配結束了,那么就需要移除
stack 就是為了明確各節點間父子關系而存在的
保證 stack 中最后一個節點,永遠是下次匹配的節點的父節點
舉個栗子,存在下面模板
stack 匹配兩個 頭標簽之后
stack = [ "div" , "section"]
看看 start 可以知道,此時 currentParent = section
然后匹配到 ,則移除 stack 中的 section,并且重設 currentParent
stack = ["div"] currentParent = "div"
再匹配到 p 的時候,p 的父節點就是 div,父子順序就是正確的了
2重新設置 stack 最后一個節點為父節點 處理文本字符串當 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 }); } }
這段代碼主要作用就是,為 父節點 添加 文本子節點
而文本子節點分為兩種類型
1、普通型,直接存為文本子節點
2、表達式型,需要經過 parseText 處理
直接以結果來定義吧
比如處理這段文本
{{isShow}}
{ expression: toString(isShow) tokens: [{@binding: "isShow"}] }
主要是為了把表達式 isShow 拿到,方便后面從實例上獲取值
好的,現在,template 處理流程所涉及的主要方法都講完了
現在用上面這些函數來走一個流程
現在有一個模板
1 開始循環 tempalte11
匹配到第一個 頭標簽 (
該 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 中被調用
最后鑒于本人能力有限,難免會有疏漏錯誤的地方,請大家多多包涵,如果有任何描述不當的地方,歡迎后臺聯系本人,有重謝
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106593.html
摘要:當字符串開頭是時,可以匹配匹配尾標簽。從結尾,找到所在位置批量閉合。 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請點擊 下面鏈接 或者 拉到 下面關注公眾號也可以吧 【Vue原理】Compile - 源碼版 之 標簽解析...
摘要:頁面這個實例,按理就需要解析兩次,但是有緩存之后就不會理清思路也就是說,其實內核就是不過是經過了兩波包裝的第一波包裝在中的內部函數中內部函數的作用是合并公共和自定義,但是相關代碼已經省略,另一個就是執行第二波包裝在中,目的是進行緩存 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 ...
摘要:寫文章不容易,點個贊唄兄弟專注源碼分享,文章分為白話版和源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于版本如果你覺得排版難看,請點擊下面鏈接或者拉到下面關注公眾號也可以吧原理源碼版之屬性解析哈哈哈,今天終 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究...
摘要:寫文章不容易,點個贊唄兄弟專注源碼分享,文章分為白話版和源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于版本如果你覺得排版難看,請點擊下面鏈接或者拉到下面關注公眾號也可以吧原理白話版終于到了要講白話的時候了 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究...
摘要:還原的難度就在于變成模板了,因為其他的什么等是原封不動的哈哈,可是直接照抄最后鑒于本人能力有限,難免會有疏漏錯誤的地方,請大家多多包涵,如果有任何描述不當的地方,歡迎后臺聯系本人,有重謝 寫文章不容易,點個贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【2.5.17】 如果你覺得排版...
閱讀 1641·2019-08-30 15:44
閱讀 2565·2019-08-30 11:19
閱讀 393·2019-08-30 11:06
閱讀 1556·2019-08-29 15:27
閱讀 3077·2019-08-29 13:44
閱讀 1621·2019-08-28 18:28
閱讀 2352·2019-08-28 18:17
閱讀 1978·2019-08-26 10:41