在緊張的一個星期的整理,筆者的前端小組每一個人都整理了一篇文章,筆者整理了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'
}
複製代碼
關於這個正則,咱們附上一個講解圖:
而關於dynamicArgAttribute, 則是大同小異:主要是多了\[[^=]+\][^\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
}
複製代碼
時間倉促,但願多多支持。