/**
* 通过循环遍历 html 模版字符串,依次处理其中的各个标签,以及标签上的属性
* @param {*} html html 模版
* @param {*} options 配置项
*/
export function parseHTML(html, options) {
const stack = []
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(html.substring(4, commentEnd), index, index + commentEnd + 3)
}
// 调整 html 和 index 变量
advance(commentEnd + 3)
continue
}
}
// 处理条件注释标签:<!--[if IE]>
// 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 (process.env.NODE_ENV !== 'production' && !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()
}
advance
/src/compiler/parser/html-parser.js
/**
* 重置 html,html = 从索引 n 位置开始的向后的所有字符
* index 为 html 在 原始的 模版字符串 中的的开始索引,也是下一次该处理的字符的开始位置
* @param {*} n 索引
*/
function advance(n) {
index += n
html = html.substring(n)
}
parseStartTag
/src/compiler/parser/html-parser.js
/**
* 解析开始标签,比如:<div id="app">
* @returns { tagName: 'div', attrs: [[xx], ...], start: index }
*/
function parseStartTag() {
const start = html.match(startTagOpen)
if (start) {
// 处理结果
const match = {
// 标签名
tagName: start[1],
// 属性,占位符
attrs: [],
// 标签的开始位置
start: index
}
/**
* 调整 html 和 index,比如:
* html = ' id="app">'
* index = 此时的索引
* start[0] = '<div'
*/
advance(start[0].length)
let end, attr
// 处理 开始标签 内的各个属性,并将这些属性放到 match.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)
}
// 开始标签的结束,end = '>' 或 end = ' />'
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
handleStartTag
/**
* 从 el.attrsList 中删除指定的属性 name
* 如果 removeFromMap 为 true,则同样删除 el.attrsMap 对象中的该属性,
* 比如 v-if、v-else-if、v-else 等属性就会被移除,
* 不过一般不会删除该对象上的属性,因为从 ast 生成 代码 期间还需要使用该对象
* 返回指定属性的值
*/
// note: this only removes the attr from the Array (attrsList) so that it
// doesn't get processed by processAttrs.
// By default it does NOT remove it from the map (attrsMap) because the map is
// needed during codegen.
export function getAndRemoveAttr (
el: ASTElement,
name: string,
removeFromMap?: boolean
): ?string {
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
}
processFor
/src/compiler/parser/index.js
/**
* 处理 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 (process.env.NODE_ENV !== 'production') {
warn(
`Invalid v-for expression: ${exp}`,
el.rawAttrsMap['v-for']
)
}
}
}
addRawAttr
/src/compiler/helpers.js
// 在 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))
}
processElement
/src/compiler/parser/index.js
/**
* 分别处理元素节点的 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
) {
// el.key = val
processKey(element)
// 确定 element 是否为一个普通元素
// determine whether this is a plain element after
// removing structural attributes
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
}
processKey
/src/compiler/parser/index.js
/**
* 处理元素上的 key 属性,设置 el.key = val
* @param {*} el
*/
function processKey(el) {
// 拿到 key 的属性值
const exp = getBindingAttr(el, 'key')
if (exp) {
// 关于 key 使用上的异常处理
if (process.env.NODE_ENV !== 'production') {
// 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
}
}
processRef
/**
* 处理作为插槽传递给组件的内容,得到:
* 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 (process.env.NODE_ENV !== 'production' && 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 (process.env.NODE_ENV !== 'production' && 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 = val
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 (process.env.NODE_ENV !== 'production') {
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 (process.env.NODE_ENV !== 'production') {
// 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
)
}
}
// 将组件的孩子添加到它的默认插槽内
// add the component's children to its default slot
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
}
}
}
}
getSlotName
/src/compiler/parser/index.js
/**
* 解析 binding,得到插槽名称以及是否为动态插槽
* @returns { name: 插槽名称, dynamic: 是否为动态插槽 }
*/
function getSlotName(binding) {
let name = binding.name.replace(slotRE, '')
if (!name) {
if (binding.name[0] !== '#') {
name = 'default'
} else if (process.env.NODE_ENV !== 'production') {
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 }
}
processSlotOutlet
/src/compiler/parser/index.js
// handle <slot/> outlets,处理自闭合 slot 标签
// 得到插槽名称,el.slotName
function processSlotOutlet(el) {
if (el.tag === 'slot') {
// 得到插槽名称
el.slotName = getBindingAttr(el, 'name')
// 提示信息,不要在 slot 标签上使用 key 属性
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')
)
}
}
}
processComponent
/**
* 通过循环遍历 html 模版字符串,依次处理其中的各个标签,以及标签上的属性
* @param {*} html html 模版
* @param {*} options 配置项
*/
export function parseHTML(html, options) {
const stack = []
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(html.substring(4, commentEnd), index, index + commentEnd + 3)
}
// 调整 html 和 index 变量
advance(commentEnd + 3)
continue
}
}
// 处理条件注释标签:<!--[if IE]>
// 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 (process.env.NODE_ENV !== 'production' && !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()
}
advance
/src/compiler/parser/html-parser.js
/**
* 重置 html,html = 从索引 n 位置开始的向后的所有字符
* index 为 html 在 原始的 模版字符串 中的的开始索引,也是下一次该处理的字符的开始位置
* @param {*} n 索引
*/
function advance(n) {
index += n
html = html.substring(n)
}
parseStartTag
/src/compiler/parser/html-parser.js
/**
* 解析开始标签,比如:<div id="app">
* @returns { tagName: 'div', attrs: [[xx], ...], start: index }
*/
function parseStartTag() {
const start = html.match(startTagOpen)
if (start) {
// 处理结果
const match = {
// 标签名
tagName: start[1],
// 属性,占位符
attrs: [],
// 标签的开始位置
start: index
}
/**
* 调整 html 和 index,比如:
* html = ' id="app">'
* index = 此时的索引
* start[0] = '<div'
*/
advance(start[0].length)
let end, attr
// 处理 开始标签 内的各个属性,并将这些属性放到 match.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)
}
// 开始标签的结束,end = '>' 或 end = ' />'
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
handleStartTag
/**
* 从 el.attrsList 中删除指定的属性 name
* 如果 removeFromMap 为 true,则同样删除 el.attrsMap 对象中的该属性,
* 比如 v-if、v-else-if、v-else 等属性就会被移除,
* 不过一般不会删除该对象上的属性,因为从 ast 生成 代码 期间还需要使用该对象
* 返回指定属性的值
*/
// note: this only removes the attr from the Array (attrsList) so that it
// doesn't get processed by processAttrs.
// By default it does NOT remove it from the map (attrsMap) because the map is
// needed during codegen.
export function getAndRemoveAttr (
el: ASTElement,
name: string,
removeFromMap?: boolean
): ?string {
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
}
processFor
/src/compiler/parser/index.js
/**
* 处理 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 (process.env.NODE_ENV !== 'production') {
warn(
`Invalid v-for expression: ${exp}`,
el.rawAttrsMap['v-for']
)
}
}
}
addRawAttr
/src/compiler/helpers.js
// 在 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))
}
processElement
/src/compiler/parser/index.js
/**
* 分别处理元素节点的 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
) {
// el.key = val
processKey(element)
// 确定 element 是否为一个普通元素
// determine whether this is a plain element after
// removing structural attributes
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
}
processKey
/src/compiler/parser/index.js
/**
* 处理元素上的 key 属性,设置 el.key = val
* @param {*} el
*/
function processKey(el) {
// 拿到 key 的属性值
const exp = getBindingAttr(el, 'key')
if (exp) {
// 关于 key 使用上的异常处理
if (process.env.NODE_ENV !== 'production') {
// 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
}
}
processRef
/**
* 处理作为插槽传递给组件的内容,得到:
* 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 (process.env.NODE_ENV !== 'production' && 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 (process.env.NODE_ENV !== 'production' && 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 = val
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 (process.env.NODE_ENV !== 'production') {
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 (process.env.NODE_ENV !== 'production') {
// 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
)
}
}
// 将组件的孩子添加到它的默认插槽内
// add the component's children to its default slot
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
}
}
}
}
getSlotName
/src/compiler/parser/index.js
/**
* 解析 binding,得到插槽名称以及是否为动态插槽
* @returns { name: 插槽名称, dynamic: 是否为动态插槽 }
*/
function getSlotName(binding) {
let name = binding.name.replace(slotRE, '')
if (!name) {
if (binding.name[0] !== '#') {
name = 'default'
} else if (process.env.NODE_ENV !== 'production') {
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 }
}
processSlotOutlet
/src/compiler/parser/index.js
// handle <slot/> outlets,处理自闭合 slot 标签
// 得到插槽名称,el.slotName
function processSlotOutlet(el) {
if (el.tag === 'slot') {
// 得到插槽名称
el.slotName = getBindingAttr(el, 'name')
// 提示信息,不要在 slot 标签上使用 key 属性
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')
)
}
}
}
processComponent