在緊張的一個星期的整理,筆者的前端小組每一個人都整理了一篇文章,筆者整理了Vue編譯模版到虛擬樹
的思想這一篇幅。建議讀者看到這篇以前,先點擊這裏預習一下整個流程的思想和思路。html
本文介紹的是Vue編譯中的parse部分的源碼分析,也就是從template 到 astElemnt的解析到程。前端
從筆者的 Vue編譯思想詳解一文中,咱們已經知道編譯個四個流程分別爲parse、optimize、code generate、render。具體細節這裏不作贅述,附上以前的一張圖。vue
本文則旨在從思想落實到源代碼分析,固然只是針對parse
這一部分的。node
筆者先列出咱們在看源碼以前,須要先預習的一些概念和準備。ios
parse的最終目標是生成具備衆多屬性的astElement樹,而這些屬性有不少則摘自標籤的一些屬性。 如 div上的v-for、v-if、v-bind等等,最終都會變成astElement的節點屬性。 這裏先給個例子:web
<div v-for="(item,index) in options" :key="item.id"></div>
到正則表達式
{ alias: "item" attrsList: [], attrsMap: {"v-for": "(item,index) in options", :key: "item.id"}, children: (2) [{…}, {…}], end: 139, for: "options", iterator1: "index", key: "item.id", parent: {type: 1, tag: "div", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …}, plain: false, rawAttrsMap: {v-for: {…}, :key: {…}}, start: 15, tag: "div", type: 1, } 複製代碼
能夠看到v-for的屬性已經被解析和從摘除出來,存在於astElement的多個屬性上面了。而摘除
的這個功能就是出自於正則強大的力量。下面先列出一些重要的正則預熱。express
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 重要1 const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 重要二 const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*` const qnameCapture = `((?:${ncname}\\:)?${ncname})` const startTagOpen = new RegExp(`^<${qnameCapture}`) const startTagClose = /^\s*(\/?)>/ const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) const doctype = /^<!DOCTYPE [^>]+>/i // #7298: escape - to avoid being pased as HTML comment when inlined in page const comment = /^<!\--/ const conditionalComment = /^<!\[/ export const onRE = /^@|^v-on:/ export const dirRE = process.env.VBIND_PROP_SHORTHAND ? /^v-|^@|^:|^\./ : /^v-|^@|^:/ export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const stripParensRE = /^\(|\)$/g // 在v-for中去除 括號用的。 const dynamicArgRE = /^\[.*\]$/ // 判斷是否爲動態屬性 const argRE = /:(.*)$/ // 配置 :xxx export const bindRE = /^:|^\.|^v-bind:/ // 匹配bind的數據,若是在組件上會放入prop裏面 不然放在attr裏面。 const propBindRE = /^\./ const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g const slotRE = /^v-slot(:|$)|^#/ const lineBreakRE = /[\r\n]/ const whitespaceRE = /\s+/g const invalidAttributeRE = /[\s"'<>\/=]/ 複製代碼
正則基礎不太好的同窗能夠先學兩篇正則基礎文章,特別詳細:數組
而且附帶上兩個網站,供你們學習正則。瀏覽器
一次性看到這麼多正則是否是有點頭暈目眩。不要慌,這裏給你們詳細講解下比較複雜的幾條正則。
1)獲取屬性的正則
attribute 和 dynamicArgAttribute 分別獲取普通屬性和動態屬性的正則表達式。 普通屬性你們必定十分熟悉了,這裏對動態屬性作下解釋。
動態屬性,就是key值可能會發生變更的屬性,vue的寫法如 v-bind:[attrName]="attrVal"
,經過改變attrName來改變傳遞的屬性的key值。(非動態屬性只能修改val值)。
咱們先對attribute
這個通用正則作一個詳細的講解:
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
很長對不對??
可是細細的拆分的化,一共五個分組。
這個分組是匹配 非空格、"、'、<、>、/、= 等符號的字符串。 主要會匹配到屬性的key值部分。以下面的屬性:
id="container" 複製代碼
([^\s"'<>/=]+)會匹配到id。
id="container" id = "container" 複製代碼
都會匹配到 = 號,第二個會把空格一塊兒匹配了。
id="container" // exp1 id='container' // exp2 id=container // exp3 複製代碼
對於exp1
,正則一
會匹配到"container"
, exp2
,正則2
匹配到'container'
,exp3
的話正則三
會匹配到container。
Vue源碼的正則基本將大多數狀況都考慮在內了。
這樣的話應該比較清晰了,咱們來歸納下:
attribute匹配的一共是三種狀況, name="xxx" name='xxx' name=xxx。
可以保證屬性的全部狀況都能包含進來。 須要注意的是正則處理後的數組的格式是:
['name','=','val','',''] 或者 ['name','=','','val',''] 或者 ['name','=','','','val'] 複製代碼
下面講源碼的時候,會知道這種數組格式是attr屬性的原始狀態,parse後期會將這種屬性處理成attrMap的形式,大體以下:
{ name:'xxx', id:'container' } 複製代碼
關於這個正則,咱們附上一個講解圖:
主要是多了\[[^=]+\][^\s"'<>\/=]*
也就是 [name] 或者 [name]key 這類狀況,附上正則詳解圖:
2)標籤處理正則
標籤主要包含開始標籤 (如<div>
)和結束標籤(如</div>
),正則分別爲如下兩個:
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*` const qnameCapture = `((?:${ncname}\\:)?${ncname})` const startTagOpen = new RegExp(`^<${qnameCapture}`) const startTagClose = /^\s*(\/?)>/ const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) 複製代碼
可以看到標籤的匹配是以qnameCapture爲基礎的,那麼這玩意又是啥呢? 其實qname就是相似於xml:xxx的這類帶冒號的標籤,因此startTagOpen是匹配<div
或<xml:xxx
的標籤。 endTag匹配的是如</div>或</xml:xxx>
的標籤
3)處理vue的標籤
export const onRE = /^@|^v-on:/ 處理綁定事件的正則 export const dirRE = process.env.VBIND_PROP_SHORTHAND ? /^v-|^@|^:|^\./ // v- | @click | :name | .stop 指令匹配 : /^v-|^@|^:/ 複製代碼
一眼就能看出來,對不對?直接進入複雜的for標籤。
for 標籤比較重要,匹配也稍微複雜點,這裏作個詳解:
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/ export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ 複製代碼
首先申明這裏的正則是依賴於attribute正則的,咱們會拿到v-for裏面的內容,舉個例子v-for="item in options"
,咱們最終會處理成一個map的形式,大體以下:
const element = { attrMap: { 'v-for':'item in options', ... } } 複製代碼
也就是說咱們會在item in options
的基礎上進行正則匹配。 先看forAliasRE
的分組,一共兩個分組分別是([\s\S]*?)
和([\s\S]*)
會分別匹配 item
和 options
。這裏舉的例子比較簡單。 實際上 in
或of
以前的內容可能會比較複雜的,如(value,key)
或者(item,index)
等,甚至可能(value,key,index)
,這個時候就是forIteratorRE
開始起做用了。 它一共兩個分組都是([^,\}\]]*)
,其實就是拿到alias
的最後兩個參數,你們都知道Vue對於Object的循環,是能夠這麼作的,例子以下:
<div v-for="(value,key,index)"> 複製代碼
而forIteratorRE
則是爲了獲取key
和index
的。最終會放在astElement的iterator1
和 iterator2
。
{ iterator1:',key', iterator2:',index' } 複製代碼
好了關於正則就說這麼多了,具體的狀況仍是得本身去看看源碼的。
依然是在開始講源碼前,先大體介紹下源碼的結構。先貼個代碼出來
function parse() { 模塊一:初始化須要的方法 模塊二: 初始化全部標記 模塊三: 開始識別並建立 astElement 樹。 } 複製代碼
模塊一大體是一些功能函數,給出代碼:
platformIsPreTag = options.isPreTag || no //判斷是否爲 pre 標籤 platformMustUseProp = options.mustUseProp || no // 判斷某個屬性是不是某個標籤的必要屬性,如selected 對於option platformGetTagNamespace = options.getTagNamespace || no // 判斷是否爲 svg or math標籤 對函數 const isReservedTag = options.isReservedTag || no // 判斷是否爲該平臺對標籤,目前vue源碼只有 web 和weex兩個平臺。 maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag) //是否可能爲組件 transforms = pluckModuleFunction(options.modules, 'transformNode') // 數組,成員是方法, 用途是摘取 staticStyle styleBinding staticClass classBinding preTransforms = pluckModuleFunction(options.modules, 'preTransformNode') // ?? postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') // ?? delimiters = options.delimiters // express標誌 function closeElement() {...} // 處理astElement對結尾函數 function trimEndingWhitespace() {...} // 處理尾部空格 function checkRootConstraints() {...} // 檢查root標籤對合格性 複製代碼
模塊二則是一些parse函數做用域內的全局標誌和存儲容器,代碼以下:
const stack = [] // 配合使用的棧 主要目的是爲了完成樹狀結構。 let root // 根節點記錄,樹頂 let currentParent // 當前父節點 let inVPre = false // 標記是否在v-pre節點 當中 let inPre = false // 是否在pre標籤當中 let warned = false 複製代碼
模塊三是核心部分,也就是解析template的部分,這個函數一旦執行完, 模塊2的root會變成一顆以astElement爲節點的dom樹。
,其代碼大體爲:
parseHTML(template,options)
複製代碼
parseHTML函數和 options 是解析的關鍵,options包括不少平臺配置和 傳入的四個處理方法。大體以下:
options = {
warn,
expectHTML: options.expectHTML, // 是否指望和瀏覽器器保證一致。
isUnaryTag: options.isUnaryTag, // 是否爲一元標籤的判斷函數
canBeLeftOpenTag: options.canBeLeftOpenTag, // 能夠直接進行閉合的標籤
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments, // 是否保留註釋
outputSourceRange: options.outputSourceRange,
// 這裏分開,上面是平臺配置、下面是處理函數。
start, // 解析處理函數(1)
end, //解析處理函數(2)
chars, //解析處理函數(3)
commend //解析處理函數(4)
}
複製代碼
筆者以前的parse思想的文章,已經介紹過兩個處理函數start和end了,一個是建立astElement另外一個是創建父子關係,其中細節會在下文中,詳細介紹,這也是本文的重點。
chars函數處理的是文本節點,commend處理的則是註釋節點。 切記這四個函數相當重要,下面會用代號講解。
Vue的html解析並不是一步到位,先來介紹一些重點的函數功能
前面咱們說到了startTagOpen
是用來匹配開始標籤的。而parseHTML
裏面的parseStartTag
函數則是利用該正則,匹配開始標籤,創立一種初始的數據結構match,保存相應的屬性,對於開始標籤裏的全部屬性,如id、class、v-bind,都會保存到match.attr中。
代碼以下:
/** * 建立match數據結構 * 初始化的狀態 * 只有 * tagName * attrs * attrs本身是個數組 也就是 正則達到的效果。。 * start * end */ function parseStartTag () { const start = html.match(startTagOpen) // 匹配開始標籤。c if (start) { const match = { // 建立相應的數據結構 tagName: start[1], attrs: [], start: index } advance(start[0].length) let end, attr //遍歷的摘取取屬性值,並保存到attrs while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) { attr.start = index advance(attr[0].length) attr.end = index match.attrs.push(attr) } if (end) { match.unarySlash = end[1] // 是否爲 一元標記 直接閉合 advance(end[0].length) match.end = index return match } } } 複製代碼
上面的while中,咱們是用開始標籤的結束符做爲結束條件的。 startTagClose的正則是
const startTagClose = /^\s*(\/?)>/
複製代碼
它自己除了判斷是否已經結束,還有一個\/?
是用來判斷是否爲一元標籤的。 一元標籤就是如<img/>
能夠只寫一個標籤的元素。這個標記後面會用到。
parseStartTag的目標是比較原始的,得到相似於
const match = { // 匹配startTag的數據結構 tagName: 'div', attrs: [ { 'id="xxx"','id','=','xxx' }, ... ], start: index, end: xxx } 複製代碼
match大體能夠歸納爲獲取標籤、屬性和位置信息。並將此傳遞給下個函數。
// parseStartTag 拿到的是 match function handleStartTag (match) { const tagName = match.tagName const unarySlash = match.unarySlash if (expectHTML) { // 是否指望和瀏覽器的解析保持一致。 if (lastTag === 'p' && isNonPhrasingTag(tagName)) { parseEndTag(lastTag) } if (canBeLeftOpenTag(tagName) && lastTag === tagName) { parseEndTag(tagName) } } const unary = isUnaryTag(tagName) || !!unarySlash // 一元判斷 const l = match.attrs.length const attrs = new Array(l) for (let i = 0; i < l; i++) { // 將attrs的 數組模式變成 { name:'xx',value:'xxx' } const args = match.attrs[i] const value = args[3] || args[4] || args[5] || '' const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ? options.shouldDecodeNewlinesForHref : options.shouldDecodeNewlines attrs[i] = { name: args[1], value: decodeAttr(value, shouldDecodeNewlines) } if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { attrs[i].start = args.start + args[0].match(/^\s*/).length attrs[i].end = args.end } } if (!unary) { // 非一元標籤處理方式 stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end }) lastTag = tagName } if (options.start) { options.start(tagName, attrs, unary, match.start, match.end) } } 複製代碼
handleStartTag的自己效果其實很是簡單直接,就是吧match的attrs從新處理,由於以前是數組結構,在這裏他們將全部的數組式attr變成一個對象,流程大體以下:
從這樣:
attrs: [ { 'id="xxx"','id','=','xxx' }, ... ], 複製代碼
變成這樣:
attrs: [ {name='id',value='xxx' }, ... ], 複製代碼
那麼其實還有些特殊處理expectHTML
和 一元標籤
。
expectHTML
是爲了處理一些異常狀況。如 p標籤的內部出現div等等、瀏覽器會特殊處理的狀況,而Vue會盡可能和瀏覽器保持一致。具體參考 p標籤標準。
最後handleStartTag會調用 從parse傳遞的start(1)函數來作處理,start函數會在下文中有詳細的講解。
parseEndTag自己的功能特別簡單就是直接調用options傳遞進來的end函數,可是咱們觀看源碼的時候會發現源碼還蠻長的。
function parseEndTag (tagName, start, end) { let pos, lowerCasedTagName if (start == null) start = index if (end == null) end = index // Find the closest opened tag of the same type if (tagName) { lowerCasedTagName = tagName.toLowerCase() for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { // If no tag name is provided, clean shop pos = 0 } if (pos >= 0) { // Close all the open elements, up the stack for (let i = stack.length - 1; i >= pos; i--) { if (process.env.NODE_ENV !== 'production' && (i > pos || !tagName) && options.warn ) { options.warn( `tag <${stack[i].tag}> has no matching end tag.`, { start: stack[i].start, end: stack[i].end } ) } if (options.end) { options.end(stack[i].tag, start, end) } } // Remove the open elements from the stack stack.length = pos lastTag = pos && stack[pos - 1].tag } else if (lowerCasedTagName === 'br') { if (options.start) { options.start(tagName, [], true, start, end) } } else if (lowerCasedTagName === 'p') { if (options.start) { options.start(tagName, [], false, start, end) } if (options.end) { options.end(tagName, start, end) } } } } 複製代碼
看起來還蠻長的,其實主要都是去執行options.end, Vue的源碼有不少的代碼量都是在處理特殊狀況,因此看起來很臃腫。這個函數的特殊狀況主要有兩種:
<div>
<span>
<p>
</div>
複製代碼
在處理div的標籤時,根據pos的位置,將pos以前的全部標籤和匹配到的標籤都會一塊兒遍歷的去執行end函數。
可能會遇到</p>
和 </br>
標籤 這個時候 p標籤會走跟瀏覽器自動補全效果,先start再end。 而br則是一元標籤,直接進入end效果。
start函數很是長。這裏截取重點部分
start() { ... let element: ASTElement = createASTElement(tag, attrs, currentParent) // 1.建立astElement ... if (!inVPre) { processPre(element) if (element.pre) { inVPre = true } } if (platformIsPreTag(element.tag)) { inPre = true } if (inVPre) { // 處理屬性兩種狀況 processRawAttrs(element) } else if (!element.processed) { // structural directives processFor(element) processIf(element) processOnce(element) } if (!root) { root = element if (process.env.NODE_ENV !== 'production') { checkRootConstraints(root) } } if (!unary) { currentParent = element stack.push(element) } else { closeElement(element) } } 複製代碼
結構以下:
{ type: 1, tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, parent, children: [] } 複製代碼
2)處理屬性 固然在這裏只是處理部分屬性,且分爲兩種狀況:
(1)pre模式 直接摘取全部屬性
(2)普通模式 分別處理processFor(element) 、processIf(element) 、 processOnce(element)。
這些函數的詳細細節,後文會有講解,這裏只是讓你們有個印象。
end函數很是短
end (tag, start, end) { const element = stack[stack.length - 1] // pop stack stack.length -= 1 currentParent = stack[stack.length - 1] if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { element.end = end } closeElement(element) }, 複製代碼
end函數第一件事就是取出當前棧的父元素賦值給currentParent,而後執行closeElement,爲的就是可以建立完整的樹節點關係。 因此closeElement纔是end函數的重點。
下面詳細解釋下closeElement
function closeElement (element) { trimEndingWhitespace(element) // 去除 未部對空格元素 if (!inVPre && !element.processed) { element = processElement(element, options) // 處理Vue相關的一些屬性關係 } // tree management if (!stack.length && element !== root) { // allow root elements with v-if, v-else-if and v-else if (root.if && (element.elseif || element.else)) { if (process.env.NODE_ENV !== 'production') { checkRootConstraints(element) } addIfCondition(root, { // 處理root的條件展現 exp: element.elseif, block: element }) } else if (process.env.NODE_ENV !== 'production') { warnOnce( `Component template should contain exactly one root element. ` + `If you are using v-if on multiple elements, ` + `use v-else-if to chain them instead.`, { start: element.start } ) } } if (currentParent && !element.forbidden) { if (element.elseif || element.else) { // 處理 elseif else 塊級 processIfConditions(element, currentParent) } else { if (element.slotScope) { // 處理slot, 將生成的各個slot的astElement 用對象展現出來。 // scoped slot // keep it in the children list so that v-else(-if) conditions can // find it as the prev node. const name = element.slotTarget || '"default"' ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element } currentParent.children.push(element) element.parent = currentParent } } // final children cleanup // filter out scoped slots element.children = element.children.filter(c => !(c: any).slotScope) // remove trailing whitespace node again trimEndingWhitespace(element) // check pre state if (element.pre) { inVPre = false } if (platformIsPreTag(element.tag)) { inPre = false } // apply post-transforms for (let i = 0; i < postTransforms.length; i++) { postTransforms[i](element, options) } } 複製代碼
主要是作了五個操做:
processElement是closeElement很是重要的一個處理函數。先把代碼貼出來。
export function processElement ( element: ASTElement, options: CompilerOptions ) { processKey(element) // determine whether this is a plain element after // removing structural attributes element.plain = ( !element.key && !element.scopedSlots && !element.attrsList.length ) processRef(element) processSlotContent(element) processSlotOutlet(element) processComponent(element) for (let i = 0; i < transforms.length; i++) { element = transforms[i](element, options) || element } processAttrs(element) return element } 複製代碼
能夠看到主要是processKey、processRef、processSlotContent、processSlotOutlet、processComponent、processAttrs和最後一個遍歷的執行的transforms。
咱們一個個來探討一下,給你們留個印象,實際上,後面會有案例詳細講解函數們的做用。
1.首先最爲簡單的是processKey和processRef,在這兩個函數處理以前,咱們的key屬性和ref屬性都是保存在astElement上面的attrs和attrsMap,通過這兩個函數以後,attrs裏面的key和ref會被幹掉,變成astElement的直屬屬性。
2.探討一下slot的處理方式,咱們知道的是,slot的具體位置是在組件中定義的,而須要替換的內容又是組件外面嵌套的代碼,Vue對這兩塊的處理是分開的。
先說組件內的屬性摘取,主要是slot標籤的name屬性,這是processSlotOutLet完成的。
// handle <slot/> outlets function processSlotOutlet (el) { if (el.tag === 'slot') { el.slotName = getBindingAttr(el, 'name') // 就是這一句了。 if (process.env.NODE_ENV !== 'production' && el.key) { warn( `\`key\` does not work on <slot> because slots are abstract outlets ` + `and can possibly expand into multiple elements. ` + `Use the key on a wrapping element instead.`, getRawBindingAttr(el, 'key') ) } } } 複製代碼
其次是摘取須要替換的內容,也就是 processSlotContent,這是是處理展現在組件內部的slot,可是在這個地方只是簡單的將給el添加兩個屬性做用域插槽的slotScope和 slotTarget,也就是目標slot。
processComponent 並非處理component,而是摘取動態組件的is屬性。 processAttrs是獲取全部的屬性和動態屬性。
transforms是處理class和style的函數數組。這裏不作贅述了。
最終生成的的ifConditions塊級的格式大體爲:
[ { exp:'showToast', block: castElement1 }, { exp:'showOther', block: castElement2 }, { exp: undefined, block: castElement3 } ] 複製代碼
這裏會將條件展現處理成一個數組,exp存放全部的展現條件,若是是else 則爲undefined。
processElement完成的slotTarget的賦值,這裏則是將全部的slot建立的astElement以對象的形式賦值給currentParent的scopedSlots。以便後期組件內部實例話的時候能夠方便去使用vm.?slot。有興趣的童鞋能夠去看看vm.$slot的初始化。
4.處理樹到父子關係,element.parent = currentParent。
5.postTransforms。
不作具體介紹了,感興趣的同窗本身去研究下吧。
chars(){ ... const children = currentParent.children ... if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { child = { type: 2, expression: res.expression, tokens: res.tokens, text } } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { child = { type: 3, text } } } 複製代碼
chars主要處理兩中文本狀況,靜態文本和表達式,舉個例子:
<div>name</div>
複製代碼
name就是靜態文本,建立的type爲3.
<div>{{name}}</div>
複製代碼
而在這個裏面name則是表達式,建立的節點type爲2。
作個總結就是:普通tag的type爲1,純文本type爲2,表達式type爲3。
comment (text: string, start, end) { // adding anyting as a sibling to the root node is forbidden // comments should still be allowed, but ignored if (currentParent) { const child: ASTText = { type: 3, text, isComment: true } if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) { child.start = start child.end = end } currentParent.children.push(child) } } 複製代碼
也是純文本,只是節點加上了一個isComment:true的標誌。
上面完成了一些重要函數的講解,下面開始識別器的探索。
咱們的主要目的是瞭解parse的主要目的和過程。不會在一些細枝末節做太多贅述。
parseHTML函數的結構以下:
function parseHTML (html, options) { const stack = [] const expectHTML = options.expectHTML const isUnaryTag = options.isUnaryTag || no const canBeLeftOpenTag = options.canBeLeftOpenTag || no let index = 0 // 座標標誌 let last, lastTag // 上一個標籤 while(html) { last = html; ... } function advance (n) { index += n html = html.substring(n) } } 複製代碼
parseHTML原理是用各個正則,不斷的識別並前進的的過程。舉個列子:
<div id="xxx">text<div> 複製代碼
startTagOpen會先匹配到<div
,而後index會前進四個位置到4,並將html去掉前面到部分,而後匹配id="xxx"
,index前進了8個位置到了13,空格也會算一個位置,html去掉這一部分。而後匹配text,最後經過endTag正則匹配<div>
。這樣就結束了。
固然了,匹配到到結果都是經過各個功能函數去處理。
先介紹下各個參數的做用,在詳細瞭解while裏面的邏輯。
這裏的核心參數一共有stack、index、last、lastTag。
他們貫穿了整個匹配線路,index相信你們已經明白是起什麼做用的了。咱們這裏分析下其餘屬性的做用域。
先看一個示例
<div>
<span>
</div>
複製代碼
這種誤寫的狀況,若是按順序識別的話,那麼span標籤永遠不會獲得end函數的處理,由於沒有識別到閉合標籤。因此stack有着檢查錯誤的功能。
stack的處理方式是,識別到開始標籤就會推入stack。識別到閉合標籤就會把對應的閉合標籤推出來。
像上面那種狀況,當識別到到時候,咱們會發現,stack裏面上面到span,下面纔是div,咱們會把這兩個一塊兒處理掉。這樣能保證生成的astElement樹的結構包括span。
請你們思考一個問題,何時咱們纔會結束?
其實就是parseHTML函數不起做用了,換句話說就是while繞了一圈發現,index沒有變,html也沒有變。 剩下的部分,咱們會看成文本處理掉。
而這塊的邏輯就是:
while(html){ last = html; .... .... if(last===html){ optios.chars(html); } } 複製代碼
有沒有恍然大悟的感受? 原來最後一步都是判斷中間的處理部分有沒有動html。last就是記錄處理前的樣式,而後在後面對比。沒有變更了就只剩下文本了。咱們直接當文本處理了。
這個標記使用的地方特別多,記錄的是上個標籤。由於有些特殊的狀況,須要判斷上個標籤。 如p標籤,記錄了上個標籤是lastTag,若是裏面出現了div等標籤,咱們會從:
<p>
<div></div>
</p>
複製代碼
變成:
<p></p>
<div></div>
<p></p>
複製代碼
緣由請參考這裏。
while的輪廓:
while(html) { last = html; if (!lastTag || !isPlainTextElement(lastTag)) { let textEnd = html.indexOf('<'); if (textEnd === 0) { ... 模塊一 } let text, rest, next if (textEnd >= 0) { ... 模塊二 } if (textEnd < 0) { text = html } if (text) { advance(text.length) } if (options.chars && text) { // 空格等 通過這個函數處理爲文本節點 options.chars(text, index - text.length, index) // 模塊三 } } else { // 模塊四 } } 複製代碼
筆者將上面的代碼大體分爲四個模塊,咱們逐一來分析講解。
if (comment.test(html)) { const commentEnd = html.indexOf('-->') if (commentEnd >= 0) { if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3) } advance(commentEnd + 3) continue } } // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment if (conditionalComment.test(html)) { const conditionalEnd = html.indexOf(']>') if (conditionalEnd >= 0) { advance(conditionalEnd + 2) continue } } // Doctype: const doctypeMatch = html.match(doctype) if (doctypeMatch) { advance(doctypeMatch[0].length) continue } // End tag: const endTagMatch = html.match(endTag) if (endTagMatch) { const curIndex = index advance(endTagMatch[0].length) parseEndTag(endTagMatch[1], curIndex, index) continue } // Start tag: const startTagMatch = parseStartTag() if (startTagMatch) { handleStartTag(startTagMatch) if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1) } continue } 複製代碼
模塊一是在let textEnd = html.indexOf('<');
的textEnd爲0的時候,才進入的。
模塊一的主要功能是匹配comment、conditionalComment、doctypeMatch、endTagMatch、startTagMatch五種狀況。他們的共同特性是匹配而且處理完後,會調用advance函數進行前進。
不一樣的是comment、endTagMatch、startTagMatch會分別進入options.comment、options.end和options.start函數。 comment函數比較簡單,這裏不作贅述來,讓咱們具體看endTagMatch和startTagMatch。
const startTagMatch = parseStartTag() if (startTagMatch) { handleStartTag(startTagMatch) if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1) } continue } 複製代碼
parseStartTag函數以前咱們有說過,除了匹配還會經過attribute正則摘取全部的屬性,並生成一個match對象。 格式以下:
match = { tagName:'xxx', attrs:[ ['id','=','container','',''], ['v-if','=','show','',''] ], start:xx, end: xx } 複製代碼
而後把結果交給handleStartTag進行處理。 handleStartTag的功能前面也有說明,主要是將原始的正則匹配到到內容,格式一下:
attrs:[ ['id','=','container','',''], ['v-if','=','show','',''] ], 複製代碼
會變成:
attrs:[ {name:'id',value:'container'}, {name:'v-if',value:'show'} ] 複製代碼
並把類match結構推入到stack當中,最後執行了options.start函數。
const endTagMatch = html.match(endTag) if (endTagMatch) { const curIndex = index advance(endTagMatch[0].length) // 前進 parseEndTag(endTagMatch[1], curIndex, index) // 進入 continue } 複製代碼
能夠看到匹配到endTag,主要是進入了parseEndTag函數。 前面已經說過,parseEndTag函數主要是判斷結束標籤,再stack到位置,並把stack尾部到這個位置之間到全部到標籤都經過options.end函數處理掉。options.end則使用closeElement去處理各個astElement到父子關係。
let text, rest, next if (textEnd >= 0) { // 有0的狀況,是由於模塊一都沒有匹配上。 rest = html.slice(textEnd) while ( !endTag.test(rest) && !startTagOpen.test(rest) && !comment.test(rest) && !conditionalComment.test(rest) ) { // < in plain text, be forgiving and treat it as text next = rest.indexOf('<', 1) // 後面是否還有 < if (next < 0) break // 沒有就不玩了 textEnd += next rest = html.slice(textEnd) } text = html.substring(0, textEnd) } 複製代碼
模塊二主要是檢查下<符號以後的代碼,其中的全部非特殊代碼都賦值到text上,換言之,就是不斷檢查有咩有endTag、startTagOpen、comment等特殊狀況,一旦檢測到就中止,將前面到多是文本到部分賦值給text。而text會看成文本信息讓模塊三去處理。
if (options.chars && text) { // 空格等 通過這個函數處理爲文本節點 options.chars(text, index - text.length, index) // 模塊三 } 複製代碼
模塊三爲類文本信息,咱們會經過options.chars函數去處理,這個函數則會進一步,判斷是否存在表達式文本,就是咱們常常綁定到值如:
{{name}}
複製代碼
這個模塊處理到是script或style標籤,這裏暫且不作贅述了,請你們自行去研究。
說了太多概念,難免會有些抽象,那麼直接給出一個具體的示例吧。
<div class="container" id="root"> <div v-if="show"> show attr bind </div> <div v-for="(item,index) in options" :key="item.id"> <span>{{item.id}}</span> <div>{{item.text}}</div> </div> </div> 複製代碼
剛進來到達while流程的是html就是完整的代碼:
html = "<div class="container" id="root"> <div v-if="show"> show attr bind </div> <div v-for="(item,index) in options" :key="item.id"> <span>{{item.id}}</span> <div>{{item.text}}</div> </div> </div>" 複製代碼
先經過parseStartTag解析<div class="container" id="root">
,獲得的結果爲:
match = { attrs:[ { 0:class="container", 1: "class", 2: "=", 3: "container", 4: undefined, 5: undefined, end: 22, groups: undefined, index: 0, input: " class="container" id="root">↵↵ <div v-if="show">↵ show attr bind↵ </div>↵↵ <div v-for="(item,index) in options" :key="item.id">↵ <span>{{item.id}}</span> ↵ <div>{{item.text}}</div>↵ </div>↵ ↵ </div>", start: 4 }, { 0: " id="root"", 1: "id" 2: "=" 3: "root" 4: undefined 5: undefined end: 32 groups: undefined index: 0 input: " id="root">↵↵ <div v-if="show">↵ show attr bind↵ </div>↵↵ <div v-for="(item,index) in options" :key="item.id">↵ <span>{{item.id}}</span> ↵ <div>{{item.text}}</div>↵ </div>↵ ↵ </div>" start: 22 }, ], end: 33 start: 0 tagName: "div" unarySlash: "" } 複製代碼
咱們能看到解析到每一個屬性,也就是attrs的對象的時候,都會用input去記錄還剩下的html。 而後將這個結果交給handleStartTag,去處理。
handleStartTag會將上面的attrs從新加工下,從數組變成:
[ { //以前是數組的形式 "name":"class", "value":"container", "start":5, "end":22 }, {"name":"id","value":"root","start":23,"end":32} ] 複製代碼
將相應的參數傳遞給options.start去處理。這個函數的入參大體以下:
options.start(
tagName, // div
attrs, // 上面處理過的attrs
unary, // 一元標籤
match.start, // 開始
match.end // 結束
)
複製代碼
那麼start函數自己呢,就去建立astElement,並處理掉v-for、v-if、v-once幾種標籤,這幾種標籤的處理方式,大體相同,從attrs去掉對應的屬性,而後直接給astElement自己建立新的屬性,下面給出處理後的格式以下:
{ if:'show', ifConditions:[ { exp:'show', block: astElement }, { exp: 'show2', block: astElement }, { exp: undefined, block: astElement } ], } 複製代碼
猜猜上述對ifConditions的第三個exp的undefined會是什麼狀況?
其實就是v-else的處理方式。
神祕面紗能夠揭開了,關於v-if 、v-else-if 不會同時做爲父節點的chidren而存在,而是隻有一個children,那就是v-if,而後其餘的會存放在ifConditions裏面。
那麼它們在源碼的具體流程是怎麼樣子的?
// 1.遇到v-if節點,則在start函數中,使用processIf函數,添加ifConditions. function processIf (el) { const exp = getAndRemoveAttr(el, 'v-if') // 添加v-if屬性 if (exp) { // 是if , el.if = exp addIfCondition(el, { // 讓咱們直接爲astElement添加一個ifConditions屬性 exp: exp, block: el }) } else { // 不是v-if 只是 給節點加上 el.else 或 el.elseif if (getAndRemoveAttr(el, 'v-else') != null) { el.else = true } const elseif = getAndRemoveAttr(el, 'v-else-if') if (elseif) { el.elseif = elseif } } } // 2. 那麼何時加上其餘條件的節點,別急別急,還記得前面的流程嗎,end函數裏面咱們會執行closeElement。 // 而這個函數有一個processIfConditions,若是不記得了,請翻上去看一看。 function processIfConditions (el, parent) { const prev = findPrevElement(parent.children) // 找到上一個節點,其實就是 倒數最後一個 if (prev && prev.if) { // 若是上一個節點是if 那麼ok,咱們就是要把當前節點推到這個節點裏面。 addIfCondition(prev, { exp: el.elseif, block: el }) } else if (process.env.NODE_ENV !== 'production') { // 天吶,你寫錯了,v-else或v-else-if以前沒有v-if,直接給錯誤。 warn( `v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` + `used on element <${el.tag}> without corresponding v-if.`, el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else'] ) } } 複製代碼
那麼v-for咱們最終會處理成什麼樣子呢?以及又是這麼處理成這種樣子的。
若是咱們的案例是這樣的:
v-for="(item,index) in list" 複製代碼
咱們獲得的結果會是:
{ for:'list', alias:'item', iterator1:'index' } 複製代碼
這裏沒有牽扯到closeElement了,直接在processFor一步到味,咱們詳細的看看吧。
// 1.processFor函數,主要是經過parseFor摘取屬性,而後經過extend拷貝給el。因此重點仍是parseFor函數。 export function processFor (el: ASTElement) { let exp if ((exp = getAndRemoveAttr(el, 'v-for'))) { // exp摘取的是v-for裏面的內容,這裏是(item,index) in list const res = parseFor(exp) // 摘取屬性 if (res) { extend(el, res) // 拷貝給el } else if (process.env.NODE_ENV !== 'production') { warn( `Invalid v-for expression: ${exp}`, el.rawAttrsMap['v-for'] ) } } } 2.詳細結果在註釋裏面了。 export function parseFor (exp: string): ?ForParseResult { // 傳入了 exp = (item,index) in list const inMatch = exp.match(forAliasRE) // 獲取了一個數組,這個正則咱們前面說了,這裏是 // ['(item,index) in list','(item,index)','list'] if (!inMatch) return const res = {} res.for = inMatch[2].trim() // list 不是嗎? const alias = inMatch[1].trim().replace(stripParensRE, '') // item,index 對嗎 const iteratorMatch = alias.match(forIteratorRE) // 這個正則咱們也說過了,也是數組 // [',index','index'] if (iteratorMatch) { res.alias = alias.replace(forIteratorRE, '').trim() res.iterator1 = iteratorMatch[1].trim() // index對嗎 if (iteratorMatch[2]) { res.iterator2 = iteratorMatch[2].trim() } } else { res.alias = alias } return res } 複製代碼
好的,結果出來了。
接着咱們對解析案例,咱們已經處理了開始標籤<div class="container" id="root">
,那麼剩下對還有
<div v-if="show"> show attr bind </div> <div v-for="(item,index) in options" :key="item.id"> <span>{{item.id}}</span> <div>{{item.text}}</div> </div> </div> 複製代碼
那麼接下來呢? parseStartTag會匹配到什麼呢?
是<div v-if="show">
嗎?
很差意思,並非。現實的template
各個標籤之間都有空格,因此在while
循環中,對於<
符號的匹配根本不會爲0,因此進不了前面所說到模塊一
,而是經過模塊二
匹配到下一個<
符號,並判斷是否爲註釋
、開始標籤
、結束標籤
的一種。 若是是,那麼從位置0
到 下一個<
符號之間的字符串
,咱們有理由相信這是一個文本節點,交給模塊三到options.chars去處理。
很顯然,從位置0到,下一個開始標籤<div v-if="show">
之間是有不少空格的,咱們會生成一個文本空節點。
而後中間的過程咱們省略的說吧。
處理<div v-if="show">
處理文本節點show attr bind
處理結束標籤
好了,這是咱們處理的第一個結束標籤 ,咱們詳細的看看吧。
// 咱們知道對於結束標籤咱們匹配到後,是直接交給parseEndTag函數處理的。這個函數容錯能力咱們不說了,前面已經
// 有了詳細的講解,咱們須要明白它會調用options.end函數。end會交給closeElement。
// closeElement會創建父子關係並處理好多好多屬性
1.processKey
2.processRef
3.processSlotContent
4.processSlotContent
5.processComponent
6.processIfConditions
....
複製代碼
到了這裏咱們還剩下:
<div v-for="(item,index) in options" :key="item.id"> <span>{{item.id}}</span> <div>{{item.text}}</div> </div> </div> 複製代碼
而後繼續省略的講解:
<div v-for="(item,index) in options" :key="item.id">
,能夠參照上面筆者描述的v-for處理方式看。<span>
開始標籤{{item.id}}
,須要注意的是,expression創建的astElement的type爲2。</span>
結束標籤<div>
開始標籤{{item.text}}
,type也是2</div>
結束標籤,結束處理方式相同。</div>
結束標籤,結束處理方式相同。</div>
結束標籤,結束處理方式相同。const match = { // 匹配startTag的數據結構 tagName: 'div', attrs: [ { 'id="xxx"','id','=','xxx' }, ... ], start: index, end: xxx } 複製代碼
時間倉促,但願多多支持。