建议PC端观看,移动端代码高亮错乱
编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。
首先来看一下 parse 的定义,在 src/compiler/parser/index.js 中:
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// 从 options 中获取方法和配置...
parseHTML(template, {
// some options...
start (tag, attrs, unary, start, end) {
// ...
},
end (tag, start, end) {
// ...
},
chars (text: string, start: number, end: number) {
// ...
},
comment (text: string, start, end) {
// ...
}
})
return root
}
parse 函数的代码很长,主要执行了 parseHTML 函数,目的是解析 HTML 模板。
1. 解析 HTML 模板
parseHTML(template, options)
对于 template 模板的解析主要是通过 parseHTML 函数,这里先展示伪代码便于理解,下文在逐步分析源码。
// src/compiler/parser/html-parser
export function parseHTML (html, options) {
const stack = [] // 栈,用于保存开始标签
const isUnaryTag = options.isUnaryTag || no // 用于判断是否是一元标签的函数,如<img>
let index = 0
let last // 用于保存上一次的 html 字符串
let lastTag // 表示上一个标签,也就是当前栈顶的元素
while (html) {
last = html
// Make sure we're not in a plaintext content element like script/style
// 不存在上一个标签,或者上一个标签不是 script/style/textarea 其中之一
// 因为像 style 标签中的内容是不用编译的
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// 处理注释标签
// 处理条件标签
// 处理doctype标签
// 处理结束标签
// 处理开始标签
}
// 处理文本
} else {
// 当上一个标签是 script/style/textarea 其中之一
}
}
}
parseHTML 的逻辑主要就是循环解析 template:
- 当不存在上一个标签,或者上一个标签不是
script/style/textarea其中之一:- 第一个字符是
<时,处理各种标签的情况。 - 否则处理文本,比如
xxxx</div>。
- 第一个字符是
- 否则处理上一个标签是
script/style/textarea其中之一的情况
parseHTML 中需要提前知道的几个概念:
- 匹配过程中会有
stack栈的概念,用来保存已解析的开始标签,栈的存在是用来维护开始标签和结束标签的一一对应关系。
- 在匹配的过程中会利用
advance函数不断前进整个模板字符串,直到字符串末尾。
function advance (n) {
index += n
html = html.substring(n)
}
为了更加直观地说明 advance 的作用,可以通过一副图表示:
调用 advance 函数:
advance(4)
- 匹配过程中用到的正则表达式如下:
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
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 passed as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/
关于正则表达式阅读吃力的话可以借助这个网站,可以生成对应的图方便理解。
了解这三个概念以后下面继续分析 parseHTML 函数:
1.1 处理注释、条件、文档类型节点
源码如下:
// comment = /^<!\--/
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
}
}
// conditionalComment = /^<!\[/
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
// doctype = /^<!DOCTYPE [^>]+>/i
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
这三种情况我们只需要调用 advance 前进到合适位置即可。
1.2 处理开始标签
源码如下:
// 处理开始标签
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
1.2.1 parseStartTag
首先通过 parseStartTag 解析开始标签:
function parseStartTag () {
// 匹配开始标签的开头
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
advance(start[0].length)
let end, attr
// 匹配开始标签的属性和结尾
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
}
}
}
结合下面的例子来分析 parseTag 函数:
<img class="test"/>
函数先通过正则表达式 startTagOpen 匹配到开始标签,此时的 start:["<img", "img"]
接着循环去匹配开始标签中的属性并添加到 match.attrs 中,直到匹配的开始标签的闭合符结束。这里属性的正则有两个分别是 dynamicArgAttribute 和 attribute。借助这个网站,我们来看看 attribute 的正则图示:
执行完 while 循环后,match.attrs 如图:
最后如果匹配到闭合符,则获取一元斜线符,前进到闭合符尾,并把当前索引赋值给 match.end。函数执行结束后 match 如图:
1.2.2 handleStartTag
回到 parseHTML 函数中,当 parseStartTag 对开始标签解析拿到 match 后,紧接着会执行 handleStartTag 对 match 做处理,handleStartTag 函数源码如下:
function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
// ...
const unary = isUnaryTag(tagName) || !!unarySlash // 是否子闭合标签
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
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) // decode value
}
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 的核心逻辑很简单:
- 先判断开始标签是否是一元标签,类似
<img>、<br/>这样。 - 根据
match.attrs构建新的attrs数组。
-
判断如果非一元标签,则往
stack里push一个对象,并且把tagName赋值给lastTag。 -
最后调用了
options.start回调函数,并传入一些参数,这个回调函数的作用稍后我会详细介绍。
1.3 处理闭合标签
源码如下:
// 处理闭合标签
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
先通过正则 endTag 匹配到闭合标签,然后前进到闭合标签末尾,最后执行 parseEndTag 方法对闭合标签做解析。
1.3.1 parseEndTag
parseEndTag 源码如下
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)
}
}
}
parseEndTag 的核心逻辑如下:
- 找到和当前闭合标签相匹配的开始标签,记录下标
pos。(如果是正常的标签匹配,那么stack的最后一个元素应该和闭合标签匹配) - 处理
pos >= 0时的情况:- 处理异常情况,比如
<div><span></div>,然后调用了options.end回调函数,这个函数稍后详细介绍。 - 把栈顶到
pos位置的都弹出,并从stack尾部拿到lastTag。
- 处理异常情况,比如
- 否则就是
pos < 0的几种情况,这里不做分析。
考虑以下错误情况:
<div><span></div>
这个时候当闭合标签为 </div> 的时候,从 stack 尾部找到的标签是 <span>,就不能匹配。
1.4 处理文本
看回 parseHTML 的代码
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// 处理注释标签
// 处理条件标签
// 处理doctype标签
// 处理结束标签
// 处理开始标签
}
// 处理文本
当 textEnd === 0 的几种情况我们已经分析完毕了,如果 textEnd 不为 0 时,说明存在文本需要处理,比如 "xxx</div>" 这种情况。
下面看看这部分的源码:
let text, rest, next
if (textEnd >= 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)
}
if (textEnd < 0) {
text = html
}
if (text) {
advance(text.length)
}
if (options.chars && text) {
options.chars(text, index - text.length, index)
}
-
接下来判断
textEnd是否大于等于0的,满足则说明到从当前位置到textEnd位置都是文本,并且如果<是纯文本中的字符,就继续找到真正的文本结束的位置,然后前进到结束的位置。 -
再继续判断
textEnd小于0的情况,则说明整个template解析完毕了,把剩余的html都赋值给了text。 -
最后调用了
options.chars回调函数,并传text参数,这个回调函数的作用稍后我会详细介绍。
1.4 当上个标签是 script/style/textarea 其中之一
看回 parseHTML 的代码
while (html) {
last = html
// Make sure we're not in a plaintext content element like script/style
// 不存在上一个标签,或者上一个标签不是 script/style/textarea 其中之一
// 因为像 style 标签中的内容是不用编译的
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// 处理注释标签
// 处理条件标签
// 处理doctype标签
// 处理结束标签
// 处理开始标签
}
// 处理文本
} else {
// 当上一个标签是 script/style/textarea 其中之一
}
}
if (!lastTag || !isPlainTextElement(lastTag)) 的情况我们已经分析完毕了。
下面看看 else 的逻辑:
else {
let endTagLength = 0
const stackedTag = lastTag.toLowerCase()
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
const rest = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length
// ...
if (options.chars) {
options.chars(text)
}
return ''
})
index += html.length - rest.length
html = rest
parseEndTag(stackedTag, index - endTagLength, index)
}
这部分逻辑也很简单,我们结合例子来分析:
<div><style>xxx</style></div>
当解析到 </style> 时:
-
lastTag和stackedTag都是style -
构建正则表达式
reStackedTag为/([\s\S]*?)(</style[^>]*>)/i。 -
然后执行如下逻辑:
const rest = html.replace(reStackedTag, function (all, text, endTag) {
// all: "xxx</style>"
// text: "xxx"
// endTag: "</style>"
endTagLength = endTag.length // 8
// ...
if (options.chars) {
options.chars(text)
}
return ''
})
主要作用就是将 "xxx</style></div>" 的 "xxx</style>" 替换为空串得到 "</div>",也就是 rest。
- 继续执行如下逻辑:
index += html.length - rest.length // index += "xxx</style></div>".length - "</div>".length
html = rest // html = "</div>"
parseEndTag(stackedTag, index - endTagLength, index)
其实就是前进到相应的位置,然后执行 parseEndTag(stackedTag, index - endTagLength, index) 处理 </style>
到这里 while 循环中的主要逻辑也介绍完了,下面分别介绍上文提到过的几个回调函数
options.startoptions.endoptions.chars
这些回调函数都是在调用 parseHTML 时最为配置对象的属性传入的。
2. options.start
当解析到开始标签的时候,最后会执行 start 回调函数,省略部分代码后函数如下:
start (tag, attrs, unary, start, end) {
// 创建 AST
let element: ASTElement = createASTElement(tag, attrs, currentParent)
// 处理 style/script 元素
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
process.env.NODE_ENV !== 'production' && warn(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
`<${tag}>` + ', as they will not be parsed.',
{ start: element.start }
)
}
if (!inVPre) {
processPre(element) // v-pre
if (element.pre) {
inVPre = true
}
}
if (inVPre) {
// 如果处于 v-pre,则跳过编译
processRawAttrs(element)
} else if (!element.processed) {
// 结构指令
processFor(element) // v-for
processIf(element) // v-if
processOnce(element) // v-once
}
if (!root) {
root = element
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(root)
}
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
}
下面分步骤来介绍这个函数:
- 创建
AST元素 - 处理
AST元素 - 压栈处理
2.1 创建 AST 元素
let element: ASTElement = createASTElement(tag, attrs, currentParent)
通过 createASTElement 函数创建 AST:
export function createASTElement (
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}
function makeAttrsMap (attrs: Array<Object>): Object {
const map = {}
for (let i = 0, l = attrs.length; i < l; i++) {
if (
process.env.NODE_ENV !== 'production' &&
map[attrs[i].name] && !isIE && !isEdge
) {
warn('duplicate attribute: ' + attrs[i].name, attrs[i])
}
map[attrs[i].name] = attrs[i].value
}
return map
}
每一个 AST 元素就是一个普通的 JavaScript 对象:
type表示AST元素类型tag表示标签名attrsList表示属性列表attrsMap表示属性映射表,key是attrs[i].name,value是attrs[i].valueparent表示父的AST元素children表示子AST元素集合
2.2 处理 AST 元素
// 判断元素是否是 style/script
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
process.env.NODE_ENV !== 'production' && warn(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
`<${tag}>` + ', as they will not be parsed.',
{ start: element.start }
)
}
// 调用模块的 preTransforms 函数
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
processPre(element) // v-pre
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
// 如果是 v-pre,则跳过编译
processRawAttrs(element)
} else if (!element.processed) {
// 结构指令
processFor(element) // v-for
processIf(element) // v-if
processOnce(element) // v-once
}
首先是判断元素是否是 script/style,如果是的话则抛出警告。
然后是对模块 preTransforms 的调用,其实所有模块的 preTransforms、 transforms 和 postTransforms 的定义都在 src/platforms/web/compiler/modules 目录中,这部分我们暂时不会介绍。
接着判断 element 是否包含各种指令通过 processXXX 做相应的处理,处理的结果就是扩展 AST 元素的属性。
这里简单介绍下 processFor 和 processIf:
2.2.1 介绍processFor
代码如下:
export function processFor (el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
const res = parseFor(exp)
if (res) {
extend(el, res)
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid v-for expression: ${exp}`,
el.rawAttrsMap['v-for']
)
}
}
}
首先通过 getAndRemoveAttr 拿到 v-for 的值,来看下 getAndRemoveAttr 函数的源码:
// 只移除从数组 attrsList 中移除 attr,因此不会被 processAttrs 处理
// 默认情况下不会从attrsMap中移除,因为这个map在codegen过程中会被使用
export function getAndRemoveAttr (
el: ASTElement,
name: string,
removeFromMap?: boolean
): ?string {
let val
if ((val = el.attrsMap[name]) != null) {
const list = el.attrsList
for (let i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
list.splice(i, 1)
break
}
}
}
if (removeFromMap) {
delete el.attrsMap[name]
}
return val
}
这个函数的逻辑非常简单,就是得到 value 值并从 el.attrsList 移除 attr,这个函数下面还会用到,后面就不重复介绍了。
回到 processFor 函数,得到 v-for 的值 exp 后,作为参数传入 parseFor 函数并执行,目的是用来解析 exp 并得到 res 对象。下面来看看 parseFor 的源码:
export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
export function parseFor (exp: string): ?ForParseResult {
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const res = {}
res.for = inMatch[2].trim()
const alias = inMatch[1].trim().replace(stripParensRE, '')
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) {
res.alias = alias.replace(forIteratorRE, '').trim()
res.iterator1 = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
res.iterator2 = iteratorMatch[2].trim()
}
} else {
res.alias = alias
}
return res
}
其实就是分别解析出 for、alias、iterator1、iterator2 等属性的值添加到 res 的元素上。比如对于 v-for="(item,index) in data" 而言,解析出:
for是dataalias是itemiterator1是index- 没有
iterator2。
再次回到 processFor 函数,parseFor 执行完毕后得到 res 对象,执行 extend(el, res),目的就是将 res 得属性拓展到 AST 元素上。
至此,processFor 函数就简单介绍完了。
2.2.2 介绍processIf
代码如下:
function processIf (el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = []
}
el.ifConditions.push(condition)
}
processIf 就是从元素中拿 v-if 指令的内容,如果拿到则给 AST 元素添加 if 属性和 ifConditions 属性;否则尝试拿 v-else 指令及 v-else-if 指令的内容,如果拿到则给 AST 元素分别添加 else 和 elseif 属性。
2.3 处理压栈
最后来看看 options.start 的剩余逻辑:
if (!root) {
root = element
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(root)
}
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
如果不存在 root 根元素,则将当前 element 作为 root,并调用 checkRootConstraints:
function checkRootConstraints (el) {
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.',
{ start: el.start }
)
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.',
el.rawAttrsMap['v-for']
)
}
}
这个函数就是用来检查 root 是不是 slot/template/v-for 的情况。
接着如果当前元素是非一元标签,将 element 保存为 currentParent,同时入栈。
否则如果是一元标签时,会执行 closeElement,这个函数稍后在 options.end 中再介绍。
需要注意的是这里的
stack和前文的stack不是同一个栈,这里的stack保存的是AST元素。
3. options.end
当解析到闭合标签的时候,最后会执行 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方法。
3.1 closeElement
function closeElement (element) {
// 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, {
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) {
// ...
} else {
// ...
currentParent.children.push(element)
element.parent = currentParent
}
}
// ...
}
当满足 !stack.length && element !== root 时,说明模板不止一个根元素
- 允许有多个根元素,但是必须是
v-if, v-else-if and v-else的情况 - 否则报错
当 currentParent 存在时,构建 AST 元素之间的父子关系。
4. options.chars
除了处理开始标签和闭合标签,我们还会在解析模板的过程中去处理一些文本内容:
chars (text: string, start: number, end: number) {
if (!currentParent) {
if (process.env.NODE_ENV !== 'production') {
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.',
{ start }
)
} else if ((text = text.trim())) {
warnOnce(
`text "${text}" outside root element will be ignored.`,
{ start }
)
}
}
return
}
const children = currentParent.children
if (text) {
let res
let child: ?ASTNode
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
}
}
if (child) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
},
文本构造的 AST 元素有 2 种类型:
- 有表达式,
type为2 - 纯文本,
type为3
比如这个例子:
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>
通过执行 parseText(text, delimiters) 对文本解析,它的定义在 src/compiler/parser/text-parsre.js 中:
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g
const buildRegex = cached(delimiters => {
const open = delimiters[0].replace(regexEscapeRE, '\\$&')
const close = delimiters[1].replace(regexEscapeRE, '\\$&')
return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})
export function parseText (
text: string,
delimiters?: [string, string]
): TextParseResult | void {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if (!tagRE.test(text)) {
return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// tag token
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
parseText 首先根据分隔符(默认是 {{}}),构造了文本匹配的正则表达式,然后再循环匹配文本,遇到普通文本就 push 到 rawTokens 和 tokens 中,如果是表达式就转换成 _s(${exp}) push 到 tokens 中,以及转换成 {@binding:exp} push 到 rawTokens 中。
对于我们的例子,tokens 就是 [_s(item),'":"',_s(index)];rawTokens 就是 [{'@binding':'item'},':',{'@binding':'index'}]。那么返回的对象如下:
return {
expression: '_s(item)+":"+_s(index)',
tokens: [{'@binding':'item'},':',{'@binding':'index'}]
}
5. 两个特殊情况
在 handleStartTag 函数中有这么一段逻辑:
function handleStartTag (match) {
// ...
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
// ...
}
expectHTML 在 Web 环境中为 true,这时有两个特殊情况的处理,我们来分析一下:
5.1 情况一
首先来看这个条件 lastTag === 'p' && isNonPhrasingTag(tagName), isNonPhrasingTag 函数定义在 src/platforms/web/compiler/util.js:
// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3
// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
export const isNonPhrasingTag = makeMap(
'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' +
'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' +
'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' +
'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' +
'title,tr,track'
)
可以看到以上元素不属于 Phrasing 元素,关于这方面的知识可以参考这篇文章。
简单来说就是:所有可以放在 p 标签内,构成段落内容的元素均属于 Phrasing 元素。那么如果 p 标签中放了非 Phrasing 元素的话会怎么样呢?比如有如下例子:
<p><div>111</div></p>
在浏览器中会被处理成这样:
所以在 Vue 的编译过程中为了与标准实现一致,会手动调用 parseEndTag 闭合标签,所以 <p><div>111</div></p> 就相当于 <p></p><div>111</div></p>。
那么最后单独的 </p> 是又怎么变成 <p></p> 的呢?
实际上当解析到 </p> 并调用 parseEndTag 时有这么一段逻辑:
else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
当 pos 小于 0 时会走到这里的 else if 逻辑,手动调用了 options.start 和 options.end 生成开始标签和结束标签的 AST,对于我们的例子来说也就是 <p></p>。
5.2 情况二
接下来看这个这个条件 canBeLeftOpenTag(tagName) && lastTag === tagName,canBeLeftOpenTag 函数也定义在 src/platforms/web/compiler/util.js
// Elements that you can, intentionally, leave open
// (and which close themselves)
export const canBeLeftOpenTag = makeMap(
'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source'
)
比如说这个嵌套 p 标签的例子:
<p>111<p>222</p></p>
在浏览器中会解析成这样:
所以在 Vue 的编译过程中为了与标准实现一致,会手动调用 parseEndTag 闭合标签,所以 <p>111<p>222</p></p> 就相当于 <p>111</p><p>222</p></p>。
同理最后单独的 </p> 变成 <p></p> 和情况一相同。