parse
2023年07月19日
一、src/compiler/parser/index.ts parse()
/**
*
* 将 HTML 字符串转换为 AST
* @param {*} template HTML 模版
* @param {*} options 平台特有的编译选项
* @returns root
*/
export function parse(template: string, options: CompilerOptions): ASTElement {
// 日志
warn = options.warn || baseWarn
// 是否为 pre 标签
platformIsPreTag = options.isPreTag || no
// 必须使用 props 进行绑定的属性
platformMustUseProp = options.mustUseProp || no
// 获取标签的命名空间
platformGetTagNamespace = options.getTagNamespace || no
// 是否是保留标签(html + svg)
const isReservedTag = options.isReservedTag || no
// 判断一个元素是否为一个组件
maybeComponent = (el: ASTElement) =>
!!(
el.component ||
el.attrsMap[':is'] ||
el.attrsMap['v-bind:is'] ||
!(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
)
// 分别获取 options.modules 下的 class、model、style 三个模块中的 transformNode、preTransformNode、postTransformNode 方法
// 负责处理元素节点上的 class、style、v-model
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
// 界定符,比如: {{}}
delimiters = options.delimiters
const stack: any[] = []
// 空格选项
const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace
// 根节点,以 root 为根,处理后的节点都会按照层级挂载到 root 下,最后 return 的就是 root,一个 ast 语法树
let root
// 当前元素的父元素
let currentParent
let inVPre = false
let inPre = false
let warned = false
function warnOnce(msg, range) {
if (!warned) {
warned = true
warn(msg, range)
}
}
function closeElement(element) {
trimEndingWhitespace(element)
if (!inVPre && !element.processed) {
element = processElement(element, options)
}
// 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 (__DEV__) {
checkRootConstraints(element)
}
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (__DEV__) {
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) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
// 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.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)
}
}
function trimEndingWhitespace(el) {
// remove trailing whitespace node
if (!inPre) {
let lastNode
while (
(lastNode = el.children[el.children.length - 1]) &&
lastNode.type === 3 &&
lastNode.text === ' '
) {
el.children.pop()
}
}
}
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']
)
}
}
/**
* 通过循环遍历 html 模版字符串,依次处理其中的各个标签,以及标签上的属性
* @param {*} html html 模版
* @param {*} options 配置项
*/
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
/**
* 主要做了以下 6 件事情:
* 1、创建 AST 对象
* 2、处理存在 v-model 指令的 input 标签,分别处理 input 为 checkbox、radio、其它的情况
* 3、处理标签上的众多指令,比如 v-pre、v-for、v-if、v-once
* 4、如果根节点 root 不存在则设置当前元素为根节点
* 5、如果当前元素为非自闭合标签则将自己 push 到 stack 数组,并记录 currentParent,在接下来处理子元素时用来告诉子元素自己的父节点是谁
* 6、如果当前元素为自闭合标签,则表示该标签要处理结束了,让自己和父元素产生关系,以及设置自己的子元素
* @param {*} tag 标签名
* @param {*} attrs [{ name: attrName, value: attrVal, start, end }, ...] 形式的属性数组
* @param {*} unary 自闭合标签
* @param {*} start 标签在 html 字符串中的开始索引
* @param {*} end 标签在 html 字符串中的结束索引
*/
start(tag, attrs, unary, start, end) {
// 检查命名空间,如果存在,则继承父命名空间
const ns =
(currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
// 创建当前标签的 AST 对象
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
// 设置命名空间
element.ns = ns
}
// 这段在非生产环境下会走,在 ast 对象上添加 一些 属性,比如 start、end
if (__DEV__) {
if (options.outputSourceRange) {
element.start = start
element.end = end
// 将属性数组解析成 { attrName: { name: attrName, value: attrVal, start, end }, ... } 形式的对象
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr
return cumulated
}, {})
}
// 验证属性是否有效,比如属性名不能包含: spaces, quotes, <, >, / or =.
attrs.forEach(attr => {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
options.outputSourceRange
? {
start: attr.start! + attr.name.indexOf(`[`),
end: attr.start! + attr.name.length
}
: undefined
)
}
})
}
// 非服务端渲染的情况下,模版中不应该出现 style、script 标签
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
__DEV__ &&
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 }
)
}
/**
* 为 element 对象分别执行 class、style、model 模块中的 preTransforms 方法
* 不过 web 平台只有 model 模块有 preTransforms 方法
* 用来处理存在 v-model 的 input 标签,但没处理 v-model 属性
* 分别处理了 input 为 checkbox、radio 和 其它的情况
* input 具体是哪种情况由 el.ifConditions 中的条件来判断
* <input v-mode="test" :type="checkbox or radio or other(比如 text)" />
*/
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
// 表示 element 是否存在 v-pre 指令,存在则设置 element.pre = true
if (!inVPre) {
processPre(element)
if (element.pre) {
// 存在 v-pre 指令,则设置 inVPre 为 true
inVPre = true
}
}
// 如果 pre 标签,则设置 inPre 为 true
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
// 说明标签上存在 v-pre 指令,这样的节点只会渲染一次,将节点上的属性都设置到 el.attrs 数组对象中,作为静态属性,数据更新时不会渲染这部分内容
// 设置 el.attrs 数组对象,每个元素都是一个属性对象 { name: attrName, value: attrVal, start, end }
processRawAttrs(element)
} else if (!element.processed) {
// 处理 v-for 属性,得到 element.for = 可迭代对象 element.alias = 别名
processFor(element)
/**
* 处理 v-if、v-else-if、v-else
* 得到 element.if = "exp",element.elseif = exp, element.else = true
* v-if 属性会额外在 element.ifConditions 数组中添加 { exp, block } 对象
*/
processIf(element)
// 处理 v-once 指令,得到 element.once = true
processOnce(element)
}
// 如果 root 不存在,则表示当前处理的元素为第一个元素,即组件的 根 元素
if (!root) {
root = element
if (__DEV__) {
// 检查根元素,对根元素有一些限制,比如:不能使用 slot 和 template 作为根元素,也不能在有状态组件的根元素上使用 v-for 指令
checkRootConstraints(root)
}
}
if (!unary) {
// 非自闭合标签,通过 currentParent 记录当前元素,下一个元素在处理的时候,就知道自己的父元素是谁
currentParent = element
// 然后将 element push 到 stack 数组,将来处理到当前元素的闭合标签时再拿出来
// 将当前标签的 ast 对象 push 到 stack 数组中,这里需要注意,在调用 options.start 方法
// 之前也发生过一次 push 操作,那个 push 进来的是当前标签的一个基本配置信息
stack.push(element)
} else {
/**
* 说明当前元素为自闭合标签,主要做了 3 件事:
* 1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性
* 2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent
* 3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中
*/
closeElement(element)
}
},
/**
* 处理结束标签
* @param {*} tag 结束标签的名称
* @param {*} start 结束标签的开始索引
* @param {*} end 结束标签的结束索引
*/
end(tag, start, end) {
// 结束标签对应的开始标签的 ast 对象
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
if (__DEV__ && options.outputSourceRange) {
element.end = end
}
/**
* 主要做了 3 件事:
* 1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性
* 2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent
* 3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中
*/
closeElement(element)
},
/**
* 处理文本,基于文本生成 ast 对象,然后将该 ast 放到它的父元素的肚子里,即 currentParent.children 数组中
*/
chars(text: string, start?: number, end?: number) {
// 异常处理,currentParent 不存在说明这段文本没有父元素
if (!currentParent) {
if (__DEV__) {
// 文本不能作为组件的根元素
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
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (
isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
// 当前父元素的所有孩子节点
const children = currentParent.children
// 对 text 进行一系列的处理,比如删除空白字符,或者存在 whitespaceOptions 选项,则 text 直接置为空或者空格
if (inPre || text.trim()) {
// 文本在 pre 标签内 或者 text.trim() 不为空
text = isTextTag(currentParent)
? text
: (decodeHTMLCached(text) as string)
} else if (!children.length) {
// 说明文本不在 pre 标签内而且 text.trim() 为空,而且当前父元素也没有孩子节点,
// 则将 text 置为空
text = ''
} else if (whitespaceOption) {
// 压缩处理
if (whitespaceOption === 'condense') {
// in condense mode, remove the whitespace node if it contains
// line break, otherwise condense to a single space
text = lineBreakRE.test(text) ? '' : ' '
} else {
text = ' '
}
} else {
text = preserveWhitespace ? ' ' : ''
}
// 如果经过处理后 text 还存在
if (text) {
if (!inPre && whitespaceOption === 'condense') {
// 不在 pre 节点中,并且配置选项中存在压缩选项,则将多个连续空格压缩为单个
text = text.replace(whitespaceRE, ' ')
}
let res
// 基于 text 生成 AST 对象
let child: ASTNode | undefined
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
}
}
// child 存在,则将 child 放到父元素的肚子里,即 currentParent.children 数组中
if (child) {
if (__DEV__ && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
},
/**
* 处理注释节点
*/
comment(text: string, start, end) {
// 禁止将任何内容作为 root 的节点的同级进行添加,注释应该被允许,但是会被忽略
// 如果 currentParent 不存在,说明注释和 root 为同级,忽略
if (currentParent) {
// 注释节点的 ast
const child: ASTText = {
type: 3,
text,
isComment: true
}
if (__DEV__ && options.outputSourceRange) {
// 记录节点的开始索引和结束索引
child.start = start
child.end = end
}
// 将当前注释节点放到父元素的 children 属性中
currentParent.children.push(child)
}
}
})
// 返回生成的 ast 对象
return root
}
二、src/compiler/parser/html-parser.ts parseHTML()
/**
* 通过循环遍历 html 模版字符串,依次处理其中的各个标签,以及标签上的属性
* @param {*} html html 模版
* @param {*} options 配置项
*/
export function parseHTML(html, options: HTMLParserOptions) {
const stack: any[] = []
const expectHTML = options.expectHTML
// 是否是自闭合标签
const isUnaryTag = options.isUnaryTag || no
// 是否可以只有开始标签
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
// 记录当前在原始 html 字符串中的开始位置
let index = 0
let last, lastTag
while (html) {
last = html
// 确保不是在 script、style、textarea 这样的纯文本元素中
if (!lastTag || !isPlainTextElement(lastTag)) {
// 找第一个 < 字符
let textEnd = html.indexOf('<')
// textEnd === 0 说明在开头找到了
// 分别处理可能找到的注释标签、条件注释标签、Doctype、开始标签、结束标签
// 每处理完一种情况,就会截断(continue)循环,并且重置 html 字符串,将处理过的标签截掉,下一次循环处理剩余的 html 字符串模版
if (textEnd === 0) {
// 处理注释标签 <!-- xx -->
if (comment.test(html)) {
// 注释标签的结束索引
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// 是否应该保留 注释
if (options.shouldKeepComment && options.comment) {
// 得到:注释内容、注释的开始索引、结束索引
options.comment(
html.substring(4, commentEnd),
index,
index + commentEnd + 3
)
}
// 调整 html 和 index 变量
advance(commentEnd + 3)
continue
}
}
// 处理条件注释标签:<!--[if IE]>
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (conditionalComment.test(html)) {
// 找到结束位置
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
// 调整 html 和 index 变量
advance(conditionalEnd + 2)
continue
}
}
// 处理 Doctype,<!DOCTYPE html>
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
/**
* 处理开始标签和结束标签是这整个函数中的核型部分,其它的不用管
* 这两部分就是在构造 element ast
*/
// 处理结束标签,比如 </div>
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
// 处理结束标签
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// 处理开始标签,比如 <div id="app">,startTagMatch = { tagName: 'div', attrs: [[xx], ...], start: index }
const startTagMatch = parseStartTag()
if (startTagMatch) {
// 进一步处理上一步得到结果,并最后调用 options.start 方法
// 真正的解析工作都是在这个 start 方法中做的
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
}
let text, rest, next
if (textEnd >= 0) {
// 能走到这儿,说明虽然在 html 中匹配到到了 <xx,但是这不属于上述几种情况,
// 它就只是一个普通的一段文本:<我是文本
// 于是从 html 中找到下一个 <,直到 <xx 是上述几种情况的标签,则结束,
// 在这整个过程中一直在调整 textEnd 的值,作为 html 中下一个有效标签的开始位置
// 截取 html 模版字符串中 textEnd 之后的内容,rest = <xx
rest = html.slice(textEnd)
// 这个 while 循环就是处理 <xx 之后的纯文本情况
// 截取文本内容,并找到有效标签的开始位置(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// 则认为 < 后面的内容为纯文本,然后在这些纯文本中再次找 <
next = rest.indexOf('<', 1)
// 如果没找到 <,则直接结束循环
if (next < 0) break
// 走到这儿说明在后续的字符串中找到了 <,索引位置为 textEnd
textEnd += next
// 截取 html 字符串模版 textEnd 之后的内容赋值给 rest,继续判断之后的字符串是否存在标签
rest = html.slice(textEnd)
}
// 走到这里,说明遍历结束,有两种情况,一种是 < 之后就是一段纯文本,要不就是在后面找到了有效标签,截取文本
text = html.substring(0, textEnd)
}
// 如果 textEnd < 0,说明 html 中就没找到 <,那说明 html 就是一段文本
if (textEnd < 0) {
text = html
}
// 将文本内容从 html 模版字符串上截取掉
if (text) {
advance(text.length)
}
// 处理文本
// 基于文本生成 ast 对象,然后将该 ast 放到它的父元素的肚子里,
// 即 currentParent.children 数组中
if (options.chars && text) {
options.chars(text, index - text.length, index)
}
} else {
// 处理 script、style、textarea 标签的闭合标签
let endTagLength = 0
// 开始标签的小写形式
const stackedTag = lastTag.toLowerCase()
const reStackedTag =
reCache[stackedTag] ||
(reCache[stackedTag] = new RegExp(
'([\\s\\S]*?)(</' + stackedTag + '[^>]*>)',
'i'
))
// 匹配并处理开始标签和结束标签之间的所有文本,比如 <script>xx</script>
const rest = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1)
}
if (options.chars) {
options.chars(text)
}
return ''
})
index += html.length - rest.length
html = rest
parseEndTag(stackedTag, index - endTagLength, index)
}
// 到这里就处理结束,如果 stack 数组中还有内容,则说明有标签没有被闭合,给出提示信息
if (html === last) {
options.chars && options.chars(html)
if (__DEV__ && !stack.length && options.warn) {
options.warn(`Mal-formatted tag at end of template: "${html}"`, {
start: index + html.length
})
}
break
}
}
// Clean up any remaining tags
parseEndTag()
function advance(n) {
index += n
html = html.substring(n)
}
function parseStartTag() {
const start = html.match(startTagOpen)
if (start) {
const match: any = {
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
}
}
}
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: ASTAttr[] = 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)
}
if (__DEV__ && 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)
}
}
function parseEndTag(tagName?: any, start?: any, end?: any) {
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 (__DEV__ && (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)
}
}
}
}
三、src/compiler/parser/html-parser.ts advance()
/**
* 重置 html,html = 从索引 n 位置开始的向后的所有字符
* index 为 html 在 原始的 模版字符串 中的的开始索引,也是下一次该处理的字符的开始位置
* @param {*} n 索引
*/
function advance(n) {
index += n
html = html.substring(n)
}
四、src/compiler/parser/html-parser.ts parseStartTag()
/**
* 解析开始标签,比如:<div id="app">
* @returns { tagName: 'div', attrs: [[xx], ...], start: index }
*/
function parseStartTag() {
const start = html.match(startTagOpen)
if (start) {
// 处理结果
const match: any = {
// 标签名
tagName: start[1],
// 属性,占位符
attrs: [],
// 标签的开始位置
start: index
}
/**
* 调整 html 和 index,比如:
* html = ' id="app">'
* index = 此时的索引
* start[0] = '<div'
*/
advance(start[0].length)
// 处理 开始标签 内的各个属性,并将这些属性放到 match.attrs 数组中
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)
}
// 开始标签的结束,end = '>' 或 end = ' />'
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
五、src/compiler/parser/html-parser.ts handleStartTag()
/**
* 进一步处理开始标签的解析结果 ——— match 对象
* 处理属性 match.attrs,如果不是自闭合标签,则将标签信息放到 stack 数组,待将来处理到它的闭合标签时再将其弹出 stack,表示该标签处理完毕,这时标签的所有信息都在 element ast 对象上了
* 接下来调用 options.start 方法处理标签,并根据标签信息生成 element ast,
* 以及处理开始标签上的属性和指令,最后将 element ast 放入 stack 数组
*
* @param {*} match { tagName: 'div', attrs: [[xx], ...], start: index }
*/
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)
}
}
// 一元标签,比如 <hr />
const unary = isUnaryTag(tagName) || !!unarySlash
// 处理 match.attrs,得到 attrs = [{ name: attrName, value: attrVal, start: xx, end: xx }, ...]
// 比如 attrs = [{ name: 'id', value: 'app', start: xx, end: xx }, ...]
const l = match.attrs.length
const attrs: ASTAttr[] = new Array(l)
for (let i = 0; i < l; i++) {
// 比如:args[3] => 'id',args[4] => '=',args[5] => 'app'
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] = { id: 'app' }
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
// 非生产环境,记录属性的开始和结束索引
if (__DEV__ && options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length
attrs[i].end = args.end
}
}
// 如果不是自闭合标签,则将标签信息放到 stack 数组中,待将来处理到它的闭合标签时再将其弹出 stack
// 如果是自闭合标签,则标签信息就没必要进入 stack 了,直接处理众多属性,将他们都设置到 element ast 对象上,就没有处理 结束标签的那一步了,这一步在处理开始标签的过程中就进行了
if (!unary) {
// 将标签信息放到 stack 数组中,{ tag, lowerCasedTag, attrs, start, end }
stack.push({
tag: tagName,
lowerCasedTag: tagName.toLowerCase(),
attrs: attrs,
start: match.start,
end: match.end
})
// 标识当前标签的结束标签为 tagName
lastTag = tagName
}
/**
* 调用 start 方法,主要做了以下 6 件事情:
* 1、创建 AST 对象
* 2、处理存在 v-model 指令的 input 标签,分别处理 input 为 checkbox、radio、其它的情况
* 3、处理标签上的众多指令,比如 v-pre、v-for、v-if、v-once
* 4、如果根节点 root 不存在则设置当前元素为根节点
* 5、如果当前元素为非自闭合标签则将自己 push 到 stack 数组,并记录 currentParent,在接下来处理子元素时用来告诉子元素自己的父节点是谁
* 6、如果当前元素为自闭合标签,则表示该标签要处理结束了,让自己和父元素产生关系,以及设置自己的子元素
*/
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
六、src/compiler/parser/html-parser.ts parseEndTag()
/**
* 解析结束标签,比如:</div>
* 最主要的事就是:
* 1、处理 stack 数组,从 stack 数组中找到当前结束标签对应的开始标签,然后调用 options.end 方法
* 2、处理完结束标签之后调整 stack 数组,保证在正常情况下 stack 数组中的最后一个元素就是下一个结束标签对应的开始标签
* 3、处理一些异常情况,比如 stack 数组最后一个元素不是当前结束标签对应的开始标签,还有就是
* br 和 p 标签单独处理
* @param {*} tagName 标签名,比如 div
* @param {*} start 结束标签的开始索引
* @param {*} end 结束标签的结束索引
*/
function parseEndTag(tagName?: any, start?: any, end?: any) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
// 倒序遍历 stack 数组,找到第一个和当前结束标签相同的标签,该标签就是结束标签对应的开始标签的描述对象
// 理论上,不出异常,stack 数组中的最后一个元素就是当前结束标签的开始标签的描述对象
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
}
// 如果在 stack 中一直没有找到相同的标签名,则 pos 就会 < 0,进行后面的 else 分支
if (pos >= 0) {
// 这个 for 循环负责关闭 stack 数组中索引 >= pos 的所有标签
// 为什么要用一个循环,上面说到正常情况下 stack 数组的最后一个元素就是我们要找的开始标签,
// 但是有些异常情况,就是有些元素没有给提供结束标签,比如:
// stack = ['span', 'div', 'span', 'h1'],当前处理的结束标签 tagName = div
// 匹配到 div,pos = 1,那索引为 2 和 3 的两个标签(span、h1)说明就没提供结束标签
// 这个 for 循环就负责关闭 div、span 和 h1 这三个标签,
// 并在开发环境为 span 和 h1 这两个标签给出 ”未匹配到结束标签的提示”
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i--) {
if (__DEV__ && (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 处理正常的结束标签
options.end(stack[i].tag, start, end)
}
}
// 将刚才处理的那些标签从数组中移除,保证数组的最后一个元素就是下一个结束标签对应的开始标签
stack.length = pos
// lastTag 记录 stack 数组中未处理的最后一个开始标签
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
// 当前处理的标签为 <br /> 标签
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
// p 标签
if (options.start) {
// 处理 <p> 标签
options.start(tagName, [], false, start, end)
}
if (options.end) {
// 处理 </p> 标签
options.end(tagName, start, end)
}
}
}
七、src/compiler/parser/index.ts createASTElement()
/**
* 为指定元素创建 AST 对象
* @param {*} tag 标签名
* @param {*} attrs 属性数组,[{ name: attrName, value: attrVal, start, end }, ...]
* @param {*} parent 父元素
* @returns { type: 1, tag, attrsList, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, parent, children: []}
*/
export function createASTElement(
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
// 节点类型
type: 1,
// 标签名
tag,
// 标签的属性数组
attrsList: attrs,
// 标签的属性对象 { attrName: attrVal, ... }
attrsMap: makeAttrsMap(attrs),
// 原始属性对象
rawAttrsMap: {},
// 父节点
parent,
// 孩子节点
children: []
}
}
八、src/platforms/web/compiler/modules/model.ts preTransformNode()
/**
* 处理存在 v-model 的 input 标签,但没处理 v-model 属性
* 分别处理了 input 为 checkbox、radio 和 其它的情况
* input 具体是哪种情况由 el.ifConditions 中的条件来判断
* <input v-mode="test" :type="checkbox or radio or other(比如 text)" />
* @param {*} el
* @param {*} options
* @returns branch0
*/
function preTransformNode(el: ASTElement, options: CompilerOptions) {
if (el.tag === 'input') {
const map = el.attrsMap
// 不存在 v-model 属性,直接结束
if (!map['v-model']) {
return
}
// 获取 :type 的值
let typeBinding
if (map[':type'] || map['v-bind:type']) {
typeBinding = getBindingAttr(el, 'type')
}
if (!map.type && !typeBinding && map['v-bind']) {
typeBinding = `(${map['v-bind']}).type`
}
// 如果存在 type 属性
if (typeBinding) {
// 获取 v-if 的值,比如: <input v-model="test" :type="checkbox" v-if="test" />
const ifCondition = getAndRemoveAttr(el, 'v-if', true)
// &&test
const ifConditionExtra = ifCondition ? `&&(${ifCondition})` : ``
// 是否存在 v-else 属性,<input v-else />
const hasElse = getAndRemoveAttr(el, 'v-else', true) != null
// 获取 v-else-if 属性的值 <inpu v-else-if="test" />
const elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true)
// 克隆一个新的 el 对象,分别处理 input 为 chekbox、radio 或 其它的情况
// 具体是哪种情况,通过 el.ifConditins 条件来判断
// 1. checkbox
const branch0 = cloneASTElement(el)
// <input v-for="item in arr" :key="item" />
// 处理 v-for 表达式,得到 branch0.for = arr, branch0.alias = item
processFor(branch0)
// 在 branch0.attrsMap 和 branch0.attrsList 对象中添加 type 属性
addRawAttr(branch0, 'type', 'checkbox')
// 分别处理元素节点的 key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其它指令和一些原生属性
processElement(branch0, options)
// 标记当前对象已经被处理过了
branch0.processed = true // prevent it from double-processed
// 得到 true&&test or false&&test,标记当前 input 是否为 checkbox
branch0.if = `(${typeBinding})==='checkbox'` + ifConditionExtra
// 在 branch0.ifConfitions 数组中放入 { exp, block } 对象
addIfCondition(branch0, {
exp: branch0.if,
block: branch0
})
// 克隆一个新的 ast 对象
// 2. add radio else-if condition
const branch1 = cloneASTElement(el)
// 获取 v-for 属性值
getAndRemoveAttr(branch1, 'v-for', true)
// 在 branch1.attrsMap 和 branch1.attrsList 对象中添加 type 属性
addRawAttr(branch1, 'type', 'radio')
// 分别处理元素节点的 key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其它指令和一些原生属性
processElement(branch1, options)
// 在 branch0.ifConfitions 数组中放入 { exp, block } 对象
addIfCondition(branch0, {
// 标记当前 input 是否为 radio
exp: `(${typeBinding})==='radio'` + ifConditionExtra,
block: branch1
})
// 3. other,input 为其它的情况
const branch2 = cloneASTElement(el)
getAndRemoveAttr(branch2, 'v-for', true)
addRawAttr(branch2, ':type', typeBinding)
processElement(branch2, options)
addIfCondition(branch0, {
exp: ifCondition!,
block: branch2
})
// 给 branch0 设置 else 或 elseif 条件
if (hasElse) {
branch0.else = true
} else if (elseIfCondition) {
branch0.elseif = elseIfCondition
}
return branch0
}
}
}
九、src/compiler/helpers.ts getBindingAttr()
/**
* 获取 el 对象上执行属性 name 的值
*/
export function getBindingAttr(
el: ASTElement,
name: string,
getStatic?: boolean
): string | undefined {
// 获取指定属性的值
const dynamicValue =
getAndRemoveAttr(el, ':' + name) || getAndRemoveAttr(el, 'v-bind:' + name)
if (dynamicValue != null) {
return parseFilters(dynamicValue)
} else if (getStatic !== false) {
const staticValue = getAndRemoveAttr(el, name)
if (staticValue != null) {
return JSON.stringify(staticValue)
}
}
}
十、src/compiler/helpers.ts getAndRemoveAttr()
/**
* 从 el.attrsList 中删除指定的属性 name
* 如果 removeFromMap 为 true,则同样删除 el.attrsMap 对象中的该属性,
* 比如 v-if、v-else-if、v-else 等属性就会被移除,
* 不过一般不会删除该对象上的属性,因为从 ast 生成 代码 期间还需要使用该对象
* 返回指定属性的值
*/
export function getAndRemoveAttr(
el: ASTElement,
name: string,
removeFromMap?: boolean
): string | undefined {
let val
// 将执行属性 name 从 el.attrsList 中移除
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
}
}
}
// 如果 removeFromMap 为 true,则从 el.attrsMap 中移除指定的属性 name
// 不过一般不会移除 el.attsMap 中的数据,因为从 ast 生成 代码 期间还需要使用该对象
if (removeFromMap) {
delete el.attrsMap[name]
}
// 返回执行属性的值
return val
}
十一、src/compiler/parser/index.ts processFor()
/**
* 处理 v-for,将结果设置到 el 对象上,得到:
* el.for = 可迭代对象,比如 arr
* el.alias = 别名,比如 item
* @param {*} el 元素的 ast 对象
*/
export function processFor(el: ASTElement) {
let exp
// 获取 el 上的 v-for 属性的值
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
// 解析 v-for 的表达式,得到 { for: 可迭代对象, alias: 别名 },比如 { for: arr, alias: item }
const res = parseFor(exp)
if (res) {
// 将 res 对象上的属性拷贝到 el 对象上
extend(el, res)
} else if (__DEV__) {
warn(`Invalid v-for expression: ${exp}`, el.rawAttrsMap['v-for'])
}
}
}
十二、src/compiler/helpers.ts addRawAttr()
// 在 el.attrsMap 和 el.attrsList 中添加指定属性 name
// add a raw attr (use this in preTransforms)
export function addRawAttr(
el: ASTElement,
name: string,
value: any,
range?: Range
) {
el.attrsMap[name] = value
el.attrsList.push(rangeSetItem({ name, value }, range))
}
十三、src/compiler/parser/index.ts processElement()
/**
* 分别处理元素节点的 key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其它指令和一些原生属性
* 然后在 el 对象上添加如下属性:
* el.key、ref、refInFor、scopedSlot、slotName、component、inlineTemplate、staticClass
* el.bindingClass、staticStyle、bindingStyle、attrs
* @param {*} element 被处理元素的 ast 对象
* @param {*} options 配置项
* @returns
*/
export function processElement(element: ASTElement, options: CompilerOptions) {
processKey(element)
// 确定 element 是否为一个普通元素
element.plain =
!element.key && !element.scopedSlots && !element.attrsList.length
// el.ref = val, el.refInFor = boolean
processRef(element)
// 处理作为插槽传递给组件的内容,得到 插槽名称、是否为动态插槽、作用域插槽的值,以及插槽中的所有子元素,子元素放到插槽对象的 children 属性中
processSlotContent(element)
// 处理自闭合的 slot 标签,得到插槽名称 => el.slotName = xx
processSlotOutlet(element)
// 处理动态组件,<component :is="compoName"></component>得到 el.component = compName,
// 以及标记是否存在内联模版,el.inlineTemplate = true of false
processComponent(element)
// 为 element 对象分别执行 class、style、model 模块中的 transformNode 方法
// 不过 web 平台只有 class、style 模块有 transformNode 方法,分别用来处理 class 属性和 style 属性
// 得到 el.staticStyle、 el.styleBinding、el.staticClass、el.classBinding
// 分别存放静态 style 属性的值、动态 style 属性的值,以及静态 class 属性的值和动态 class 属性的值
for (let i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element
}
/**
* 处理元素上的所有属性:
* v-bind 指令变成:el.attrs 或 el.dynamicAttrs = [{ name, value, start, end, dynamic }, ...],
* 或者是必须使用 props 的属性,变成了 el.props = [{ name, value, start, end, dynamic }, ...]
* v-on 指令变成:el.events 或 el.nativeEvents = { name: [{ value, start, end, modifiers, dynamic }, ...] }
* 其它指令:el.directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...]
* 原生属性:el.attrs = [{ name, value, start, end }],或者一些必须使用 props 的属性,变成了:
* el.props = [{ name, value: true, start, end, dynamic }]
*/
processAttrs(element)
return element
}
十四、src/compiler/parser/index.ts processKey()
/**
* 处理元素上的 key 属性,设置 el.key = val
* @param {*} el
*/
function processKey(el) {
// 拿到 key 的属性值
const exp = getBindingAttr(el, 'key')
if (exp) {
// 关于 key 使用上的异常处理
if (__DEV__) {
// template 标签不允许设置 key
if (el.tag === 'template') {
warn(
`<template> cannot be keyed. Place the key on real elements instead.`,
getRawBindingAttr(el, 'key')
)
}
// 不要在 <transition=group> 的子元素上使用 v-for 的 index 作为 key,这和没用 key 没什么区别
if (el.for) {
const iterator = el.iterator2 || el.iterator1
const parent = el.parent
if (
iterator &&
iterator === exp &&
parent &&
parent.tag === 'transition-group'
) {
warn(
`Do not use v-for index as key on <transition-group> children, ` +
`this is the same as not using keys.`,
getRawBindingAttr(el, 'key'),
true /* tip */
)
}
}
}
// 设置 el.key = exp
el.key = exp
}
}
十五、src/compiler/parser/index.ts processRef()
/**
* 处理元素上的 ref 属性
* el.ref = refVal
* el.refInFor = boolean
* @param {*} el
*/
function processRef(el) {
const ref = getBindingAttr(el, 'ref')
if (ref) {
el.ref = ref
// 判断包含 ref 属性的元素是否包含在具有 v-for 指令的元素内或后代元素中
// 如果是,则 ref 指向的则是包含 DOM 节点或组件实例的数组
el.refInFor = checkInFor(el)
}
}
十七、src/compiler/parser/index.ts processSlotContent()
/**
* 处理作为插槽传递给组件的内容,得到:
* slotTarget => 插槽名
* slotTargetDynamic => 是否为动态插槽
* slotScope => 作用域插槽的值
* 直接在 <comp> 标签上使用 v-slot 语法时,将上述属性放到 el.scopedSlots 对象上,其它情况直接放到 el 对象上
* handle content being passed to a component as slot,
* e.g. <template slot="xxx">, <div slot-scope="xxx">
*/
function processSlotContent(el) {
let slotScope
if (el.tag === 'template') {
// template 标签上使用 scope 属性的提示
// scope 已经弃用,并在 2.5 之后使用 slot-scope 代替
// slot-scope 即可以用在 template 标签也可以用在普通标签上
slotScope = getAndRemoveAttr(el, 'scope')
/* istanbul ignore if */
if (__DEV__ && slotScope) {
warn(
`the "scope" attribute for scoped slots have been deprecated and ` +
`replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
`can also be used on plain elements in addition to <template> to ` +
`denote scoped slots.`,
el.rawAttrsMap['scope'],
true
)
}
// el.slotScope = val
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
/* istanbul ignore if */
if (__DEV__ && el.attrsMap['v-for']) {
// 元素不能同时使用 slot-scope 和 v-for,v-for 具有更高的优先级
// 应该用 template 标签作为容器,将 slot-scope 放到 template 标签上
warn(
`Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
`(v-for takes higher priority). Use a wrapper <template> for the ` +
`scoped slot to make it clearer.`,
el.rawAttrsMap['slot-scope'],
true
)
}
el.slotScope = slotScope
}
// 获取 slot 属性的值
// slot="xxx",老旧的具名插槽的写法
const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {
// el.slotTarget = 插槽名(具名插槽)
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
// 动态插槽名
el.slotTargetDynamic = !!(
el.attrsMap[':slot'] || el.attrsMap['v-bind:slot']
)
// preserve slot as an attribute for native shadow DOM compat
// only for non-scoped slots.
if (el.tag !== 'template' && !el.slotScope) {
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
}
}
// 2.6 v-slot syntax
if (process.env.NEW_SLOT_SYNTAX) {
if (el.tag === 'template') {
// v-slot 在 tempalte 标签上,得到 v-slot 的值
// v-slot on <template>
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
// 异常提示
if (__DEV__) {
if (el.slotTarget || el.slotScope) {
warn(`Unexpected mixed usage of different slot syntaxes.`, el)
}
if (el.parent && !maybeComponent(el.parent)) {
// <template v-slot> 只能出现在组件的根位置,比如:
// <comp>
// <template v-slot>xx</template>
// </comp>
// 而不能是
// <comp>
// <div>
// <template v-slot>xxx</template>
// </div>
// </comp>
warn(
`<template v-slot> can only appear at the root level inside ` +
`the receiving component`,
el
)
}
}
// 得到插槽名称
const { name, dynamic } = getSlotName(slotBinding)
// 插槽名
el.slotTarget = name
// 是否为动态插槽
el.slotTargetDynamic = dynamic
// 作用域插槽的值
el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
}
} else {
// 处理组件上的 v-slot,<comp v-slot:header />
// slotBinding = { name: "v-slot:header", value: "", start, end}
// v-slot on component, denotes default slot
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
// 异常提示
if (__DEV__) {
// el 不是组件的话,提示,v-slot 只能出现在组件上或 template 标签上
if (!maybeComponent(el)) {
warn(
`v-slot can only be used on components or <template>.`,
slotBinding
)
}
if (el.slotScope || el.slotTarget) {
warn(`Unexpected mixed usage of different slot syntaxes.`, el)
}
// 为了避免作用域歧义,当存在其他命名槽时,默认槽也应该使用<template>语法
if (el.scopedSlots) {
warn(
`To avoid scope ambiguity, the default slot should also use ` +
`<template> syntax when there are other named slots.`,
slotBinding
)
}
}
// 将组件的孩子添加到它的默认插槽内
const slots = el.scopedSlots || (el.scopedSlots = {})
// 获取插槽名称以及是否为动态插槽
const { name, dynamic } = getSlotName(slotBinding)
// 创建一个 template 标签的 ast 对象,用于容纳插槽内容,父级是 el
const slotContainer = (slots[name] = createASTElement(
'template',
[],
el
))
// 插槽名
slotContainer.slotTarget = name
// 是否为动态插槽
slotContainer.slotTargetDynamic = dynamic
// 所有的孩子,将每一个孩子的 parent 属性都设置为 slotContainer
slotContainer.children = el.children.filter((c: any) => {
if (!c.slotScope) {
// 给插槽内元素设置 parent 属性为 slotContainer,也就是 template 元素
c.parent = slotContainer
return true
}
})
slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
// remove children as they are returned from scopedSlots now
el.children = []
// mark el non-plain so data gets generated
el.plain = false
}
}
}
}
十八、src/compiler/parser/index.ts getSlotName()
/**
* 解析 binding,得到插槽名称以及是否为动态插槽
* @returns { name: 插槽名称, dynamic: 是否为动态插槽 }
*/
function getSlotName(binding) {
let name = binding.name.replace(slotRE, '')
if (!name) {
if (binding.name[0] !== '#') {
name = 'default'
} else if (__DEV__) {
warn(`v-slot shorthand syntax requires a slot name.`, binding)
}
}
return dynamicArgRE.test(name)
? // dynamic [name]
{ name: name.slice(1, -1), dynamic: true }
: // static name
{ name: `"${name}"`, dynamic: false }
}
十九、src/compiler/parser/index.ts processSlotOutlet()
// handle <slot/> outlets
function processSlotOutlet(el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name')
if (__DEV__ && 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')
)
}
}
}
二十、src/compiler/parser/index.ts processComponent()
/**
* 处理动态组件,<component :is="compName"></component>
* 得到 el.component = compName
*/
function processComponent(el) {
let binding
// 解析 is 属性,得到属性值,即组件名称,el.component = compName
if ((binding = getBindingAttr(el, 'is'))) {
el.component = binding
}
// <component :is="compName" inline-template>xx</component>
// 组件上存在 inline-template 属性,进行标记:el.inlineTemplate = true
// 表示组件开始和结束标签内的内容作为组件模版出现,而不是作为插槽别分发,方便定义组件模版
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true
}
}
二十一、src/platforms/web/compiler/modules/class.ts transformNode()
/**
* 处理元素上的 class 属性
* 静态的 class 属性值赋值给 el.staticClass 属性
* 动态的 class 属性值赋值给 el.classBinding 属性
*/
function transformNode(el: ASTElement, options: CompilerOptions) {
// 日志
const warn = options.warn || baseWarn
// 获取元素上静态 class 属性的值 xx,<div class="xx"></div>
const staticClass = getAndRemoveAttr(el, 'class')
if (__DEV__ && staticClass) {
const res = parseText(staticClass, options.delimiters)
// 提示,同 style 的提示一样,不能使用 <div class="{{ val}}"></div>,请用
// <div :class="val"></div> 代替
if (res) {
warn(
`class="${staticClass}": ` +
'Interpolation inside attributes has been removed. ' +
'Use v-bind or the colon shorthand instead. For example, ' +
'instead of <div class="{{ val }}">, use <div :class="val">.',
el.rawAttrsMap['class']
)
}
}
// 静态 class 属性值赋值给 el.staticClass
if (staticClass) {
el.staticClass = JSON.stringify(staticClass.replace(/\s+/g, ' ').trim())
}
// 获取动态绑定的 class 属性值,并赋值给 el.classBinding
const classBinding = getBindingAttr(el, 'class', false /* getStatic */)
if (classBinding) {
el.classBinding = classBinding
}
}
二十二、src/platforms/web/compiler/modules/style.ts transformNode()
/**
* 从 el 上解析出静态的 style 属性和动态绑定的 style 属性,分别赋值给:
* el.staticStyle 和 el.styleBinding
* @param {*} el
* @param {*} options
*/
function transformNode(el: ASTElement, options: CompilerOptions) {
// 日志
const warn = options.warn || baseWarn
const staticStyle = getAndRemoveAttr(el, 'style')
// <div style="xx"></div>
// 获取 style 属性
if (staticStyle) {
// 提示,如果从 xx 中解析到了界定符,说明是一个动态的 style,
// 比如 <div style="{{ val }}"></div>则给出提示:
// 动态的 style 请使用 <div :style="val"></div>
if (__DEV__) {
const res = parseText(staticStyle, options.delimiters)
if (res) {
warn(
`style="${staticStyle}": ` +
'Interpolation inside attributes has been removed. ' +
'Use v-bind or the colon shorthand instead. For example, ' +
'instead of <div style="{{ val }}">, use <div :style="val">.',
el.rawAttrsMap['style']
)
}
}
// 将静态的 style 样式赋值给 el.staticStyle
el.staticStyle = JSON.stringify(parseStyleText(staticStyle))
}
// 获取动态绑定的 style 属性,比如 <div :style="{{ val }}"></div>
const styleBinding = getBindingAttr(el, 'style', false /* getStatic */)
if (styleBinding) {
// 赋值给 el.styleBinding
el.styleBinding = styleBinding
}
}
二十三、src/compiler/parser/index.ts processAttrs()
/**
* 处理元素上的所有属性:
* v-bind 指令变成:el.attrs 或 el.dynamicAttrs = [{ name, value, start, end, dynamic }, ...],
* 或者是必须使用 props 的属性,变成了 el.props = [{ name, value, start, end, dynamic }, ...]
* v-on 指令变成:el.events 或 el.nativeEvents = { name: [{ value, start, end, modifiers, dynamic }, ...] }
* 其它指令:el.directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...]
* 原生属性:el.attrs = [{ name, value, start, end }],或者一些必须使用 props 的属性,变成了:
* el.props = [{ name, value: true, start, end, dynamic }]
*/
function processAttrs(el) {
// list = [{ name, value, start, end }, ...]
const list = el.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
// 属性名
name = rawName = list[i].name
// 属性值
value = list[i].value
// 说明该属性是一个指令
if (dirRE.test(name)) {
// 元素上存在指令,将元素标记动态元素
el.hasBindings = true
// modifiers,在属性名上解析修饰符,比如 xx.lazy
modifiers = parseModifiers(name.replace(dirRE, ''))
// 为 .props 修饰符支持 .foo 速记写法
if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
;(modifiers || (modifiers = {})).prop = true
name = `.` + name.slice(1).replace(modifierRE, '')
} else if (modifiers) {
// 属性中的修饰符去掉,得到一个干净的属性名
name = name.replace(modifierRE, '')
}
// v-bind, <div :id="test"></div>
if (bindRE.test(name)) {
// 处理 v-bind 指令属性,最后得到 el.attrs 或者 el.dynamicAttrs = [{ name, value, start, end, dynamic }, ...]
// 属性名,比如:id
name = name.replace(bindRE, '')
// 属性值,比如:test
value = parseFilters(value)
// 是否为动态属性 <div :[id]="test"></div>
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
// 如果是动态属性,则去掉属性两侧的方括号 []
name = name.slice(1, -1)
}
// 提示,动态属性值不能为空字符串
if (__DEV__ && value.trim().length === 0) {
warn(
`The value for a v-bind expression cannot be empty. Found in "v-bind:${name}"`
)
}
// 存在修饰符
if (modifiers) {
if (modifiers.prop && !isDynamic) {
name = camelize(name)
if (name === 'innerHtml') name = 'innerHTML'
}
if (modifiers.camel && !isDynamic) {
name = camelize(name)
}
// 处理 sync 修饰符
if (modifiers.sync) {
syncGen = genAssignmentCode(value, `$event`)
if (!isDynamic) {
addHandler(
el,
`update:${camelize(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
if (hyphenate(name) !== camelize(name)) {
addHandler(
el,
`update:${hyphenate(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
}
} else {
// handler w/ dynamic event name
addHandler(
el,
`"update:"+(${name})`,
syncGen,
null,
false,
warn,
list[i],
true // dynamic
)
}
}
}
if (
(modifiers && modifiers.prop) ||
(!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))
) {
// 将属性对象添加到 el.props 数组中,表示这些属性必须通过 props 设置
// el.props = [{ name, value, start, end, dynamic }, ...]
addProp(el, name, value, list[i], isDynamic)
} else {
// 将属性添加到 el.attrs 数组或者 el.dynamicAttrs 数组
addAttr(el, name, value, list[i], isDynamic)
}
} else if (onRE.test(name)) {
// v-on, 处理事件,<div @click="test"></div>
// 属性名,即事件名
name = name.replace(onRE, '')
// 是否为动态属性
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
// 动态属性,则获取 [] 中的属性名
name = name.slice(1, -1)
}
// 处理事件属性,将属性的信息添加到 el.events 或者 el.nativeEvents 对象上,格式:
// el.events = [{ value, start, end, modifiers, dynamic }, ...]
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
} else {
// normal directives,其它的普通指令
// 得到 el.directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...]
name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
let arg = argMatch && argMatch[1]
isDynamic = false
if (arg) {
name = name.slice(0, -(arg.length + 1))
if (dynamicArgRE.test(arg)) {
arg = arg.slice(1, -1)
isDynamic = true
}
}
addDirective(
el,
name,
rawName,
value,
arg,
isDynamic,
modifiers,
list[i]
)
if (__DEV__ && name === 'model') {
checkForAliasModel(el, value)
}
}
} else {
// 当前属性不是指令
if (__DEV__) {
const res = parseText(value, delimiters)
if (res) {
warn(
`${name}="${value}": ` +
'Interpolation inside attributes has been removed. ' +
'Use v-bind or the colon shorthand instead. For example, ' +
'instead of <div id="{{ val }}">, use <div :id="val">.',
list[i]
)
}
}
// 将属性对象放到 el.attrs 数组中,el.attrs = [{ name, value, start, end }]
addAttr(el, name, JSON.stringify(value), list[i])
// #6887 firefox doesn't update muted state if set via attribute
// even immediately after element creation
if (
!el.component &&
name === 'muted' &&
platformMustUseProp(el.tag, el.attrsMap.type, name)
) {
addProp(el, name, 'true', list[i])
}
}
}
}
二十四、src/compiler/helpers.ts addHandler()
/**
* 处理事件属性,将事件属性添加到 el.events 对象或者 el.nativeEvents 对象中,格式:
* el.events[name] = [{ value, start, end, modifiers, dynamic }, ...]
* 其中用了大量的篇幅在处理 name 属性带修饰符 (modifier) 的情况
* @param {*} el ast 对象
* @param {*} name 属性名,即事件名
* @param {*} value 属性值,即事件回调函数名
* @param {*} modifiers 修饰符
* @param {*} important
* @param {*} warn 日志
* @param {*} range
* @param {*} dynamic 属性名是否为动态属性
*/
export function addHandler(
el: ASTElement,
name: string,
value: string,
modifiers?: ASTModifiers | null,
important?: boolean,
warn?: Function,
range?: Range,
dynamic?: boolean
) {
// modifiers 是一个对象,如果传递的参数为空,则给一个冻结的空对象
modifiers = modifiers || emptyObject
// 提示:prevent 和 passive 修饰符不能一起使用
// warn prevent and passive modifier
/* istanbul ignore if */
if (__DEV__ && warn && modifiers.prevent && modifiers.passive) {
warn(
"passive and prevent can't be used together. " +
"Passive handler can't prevent default event.",
range
)
}
// 标准化 click.right 和 click.middle,它们实际上不会被真正的触发,从技术讲他们是它们
// 是特定于浏览器的,但至少目前位置只有浏览器才具有右键和中间键的点击
// normalize click.right and click.middle since they don't actually fire
// this is technically browser-specific, but at least for now browsers are
// the only target envs that have right/middle clicks.
if (modifiers.right) {
// 右键
if (dynamic) {
// 动态属性
name = `(${name})==='click'?'contextmenu':(${name})`
} else if (name === 'click') {
// 非动态属性,name = contextmenu
name = 'contextmenu'
// 删除修饰符中的 right 属性
delete modifiers.right
}
} else if (modifiers.middle) {
// 中间键
if (dynamic) {
// 动态属性,name => mouseup 或者 ${name}
name = `(${name})==='click'?'mouseup':(${name})`
} else if (name === 'click') {
// 非动态属性,mouseup
name = 'mouseup'
}
}
/**
* 处理 capture、once、passive 这三个修饰符,通过给 name 添加不同的标记来标记这些修饰符
*/
// check capture modifier
if (modifiers.capture) {
delete modifiers.capture
// 给带有 capture 修饰符的属性,加上 ! 标记
name = prependModifierMarker('!', name, dynamic)
}
if (modifiers.once) {
delete modifiers.once
// once 修饰符加 ~ 标记
name = prependModifierMarker('~', name, dynamic)
}
/* istanbul ignore if */
if (modifiers.passive) {
delete modifiers.passive
// passive 修饰符加 & 标记
name = prependModifierMarker('&', name, dynamic)
}
let events
if (modifiers.native) {
// native 修饰符, 监听组件根元素的原生事件,将事件信息存放到 el.nativeEvents 对象中
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
if (modifiers !== emptyObject) {
// 说明有修饰符,将修饰符对象放到 newHandler 对象上
// { value, dynamic, start, end, modifiers }
newHandler.modifiers = modifiers
}
// 将配置对象放到 events[name] = [newHander, handler, ...]
const handlers = events[name]
/* istanbul ignore if */
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
二十五、src/compiler/parser/index.ts addIfCondition()
/**
* 将传递进来的条件对象放进 el.ifConditions 数组中
*/
export function addIfCondition(el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = []
}
el.ifConditions.push(condition)
}
二十六、src/compiler/parser/index.ts processPre()
/**
* 如果元素上存在 v-pre 指令,则设置 el.pre = true
*/
function processPre(el) {
if (getAndRemoveAttr(el, 'v-pre') != null) {
el.pre = true
}
}
二十七、src/compiler/parser/index.ts processRawAttrs()
/**
* 设置 el.attrs 数组对象,每个元素都是一个属性对象 { name: attrName, value: attrVal, start, end }
*/
function processRawAttrs(el) {
const list = el.attrsList
const len = list.length
if (len) {
const attrs: Array<ASTAttr> = (el.attrs = new Array(len))
for (let i = 0; i < len; i++) {
attrs[i] = {
name: list[i].name,
value: JSON.stringify(list[i].value)
}
if (list[i].start != null) {
attrs[i].start = list[i].start
attrs[i].end = list[i].end
}
}
} else if (!el.pre) {
// non root node in pre blocks with no attributes
el.plain = true
}
}
二十八、src/compiler/parser/index.ts processIf()
/**
* 处理 v-if、v-else-if、v-else
* 得到 el.if = "exp",el.elseif = exp, el.else = true
* v-if 属性会额外在 el.ifConditions 数组中添加 { exp, block } 对象
*/
function processIf(el) {
// 获取 v-if 属性的值,比如 <div v-if="test"></div>
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
// el.if = "test"
el.if = exp
// 在 el.ifConditions 数组中添加 { exp, block }
addIfCondition(el, {
exp: exp,
block: el
})
} else {
// 处理 v-else,得到 el.else = true
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
// 处理 v-else-if,得到 el.elseif = exp
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
二十九、src/compiler/parser/index.ts processOnce()
/**
* 处理 v-once 指令,得到 el.once = true
* @param {*} el
*/
function processOnce(el) {
const once = getAndRemoveAttr(el, 'v-once')
if (once != null) {
el.once = true
}
}
三十、src/compiler/parser/index.ts checkRootConstraints()
/**
* 检查根元素:
* 不能使用 slot 和 template 标签作为组件的根元素
* 不能在有状态组件的 根元素 上使用 v-for 指令,因为它会渲染出多个元素
* @param {*} el
*/
function checkRootConstraints(el) {
// 不能使用 slot 和 template 标签作为组件的根元素
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 }
)
}
// 不能在有状态组件的 根元素 上使用 v-for,因为它会渲染出多个元素
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']
)
}
}
三十一、src/compiler/parser/index.ts closeElement()
/**
* 主要做了 3 件事:
* 1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性
* 2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent
* 3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中
*/
function closeElement(element) {
// 移除节点末尾的空格,当前 pre 标签内的元素除外
trimEndingWhitespace(element)
// 当前元素不再 pre 节点内,并且也没有被处理过
if (!inVPre && !element.processed) {
// 分别处理元素节点的 key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其它指令和一些原生属性
element = processElement(element, options)
}
// 处理根节点上存在 v-if、v-else-if、v-else 指令的情况
// 如果根节点存在 v-if 指令,则必须还提供一个具有 v-else-if 或者 v-else 的同级别节点,防止根元素不存在
// 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 (__DEV__) {
// 检查根元素
checkRootConstraints(element)
}
// 给根元素设置 ifConditions 属性,root.ifConditions = [{ exp: element.elseif, block: element }, ...]
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (__DEV__) {
// 提示,表示不应该在 根元素 上只使用 v-if,应该将 v-if、v-else-if 一起使用,保证组件只有一个根元素
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 }
)
}
}
// 让自己和父元素产生关系
// 将自己放到父元素的 children 数组中,然后设置自己的 parent 属性为 currentParent
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
// 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
}
}
// 设置自己的子元素
// 将自己的所有非插槽的子元素设置到 element.children 数组中
// final children cleanup
// filter out scoped slots
element.children = element.children.filter(c => !c.slotScope)
// remove trailing whitespace node again
trimEndingWhitespace(element)
// check pre state
if (element.pre) {
inVPre = false
}
if (platformIsPreTag(element.tag)) {
inPre = false
}
// 分别为 element 执行 model、class、style 三个模块的 postTransform 方法
// 但是 web 平台没有提供该方法
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
三十二、src/compiler/parser/index.ts trimEndingWhitespace()
/**
* 删除元素中空白的文本节点,比如:<div> </div>,删除 div 元素中的空白节点,将其从元素的 children 属性中移出去
*/
function trimEndingWhitespace(el) {
// remove trailing whitespace node
if (!inPre) {
let lastNode
while (
(lastNode = el.children[el.children.length - 1]) &&
lastNode.type === 3 &&
lastNode.text === ' '
) {
el.children.pop()
}
}
}
三十三、src/compiler/parser/index.ts processIfConditions()
function processIfConditions(el, parent) {
// 找到 parent.children 中的最后一个元素节点
const prev = findPrevElement(parent.children)
if (prev && prev.if) {
addIfCondition(prev, {
exp: el.elseif,
block: el
})
} else if (__DEV__) {
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']
)
}
}
三十四、src/compiler/parser/index.ts findPrevElement()
/**
* 找到 children 中的最后一个元素节点
*/
function findPrevElement(children: Array<any>): ASTElement | void {
let i = children.length
while (i--) {
if (children[i].type === 1) {
return children[i]
} else {
if (__DEV__ && children[i].text !== ' ') {
warn(
`text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
`will be ignored.`,
children[i]
)
}
children.pop()
}
}
}