源码版本:3.2.20
文件位置:
compiler-core/src/parse.ts
贯穿整个解析流程的对象
解析上下文对象,保存着当前的解析进度、解析配置项和源码字符串等信息,下文使用 context 表示。类型定义如下:
export interface ParserContext {
options: MergedParserOptions /* 解析配置对象 */
readonly originalSource: string /* 模板字符串源码 */
source: string /* 剩余待解析的模板字符串 */
/* cursor */
offset: number /* 当前解析位置,相对于源码字符串 */
line: number /* 当前解析位置, 映射源码的行数 */
column: number /* 当前解析位置,映射源码的列数 */
inPre: boolean /* 当前处于 <pre> 元素内 */
inVPre: boolean /* 当前元素使用 v-pre 指令 */
onWarn: NonNullable<ErrorHandlingOptions['onWarn']>
}
创建 context
export const NO = () => false
// 默认解析配置对象
export const defaultParserOptions: MergedParserOptions = {
delimiters: [`{{`, `}}`],
getNamespace: () => Namespaces.HTML,
getTextMode: () => TextModes.DATA,
isVoidTag: NO,
isPreTag: NO,
isCustomElement: NO,
decodeEntities: (rawText: string): string =>
rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
onError: defaultOnError,
onWarn: defaultOnWarn,
comments: __DEV__
}
// 创建解析上下文对象
function createParserContext(
content: string,
rawOptions: ParserOptions
): ParserContext {
const options = extend({}, defaultParserOptions)
let key: keyof ParserOptions
// 自定义配置覆盖默认配置
for (key in rawOptions) {
options[key] =
rawOptions[key] === undefined
? defaultParserOptions[key]
: rawOptions[key]
}
return {
options,
column: 1,
line: 1,
offset: 0,
originalSource: content,
source: content,
inPre: false,
inVPre: false,
onWarn: options.onWarn
}
}
Vue 提供了默认的解析配置对象,使用者也可以根据自己的需求自定义解析时的配置。返回的 context 对象中的 originalSource 为可读属性,用于解析的是 source 属性
解析的入口函数
parse.ts 导出的函数对象只有一个,baseParse 接收源码字符串和自定义的解析配置对象,返回解析生成的抽象语法树:
export function baseParse(
content: string,
options: ParserOptions = {}
): RootNode {
// 创建解析上下文对象,合并解析配置
const context = createParserContext(content, options)
// 返回根节点
return createRoot(/* 创建根节点 */
parseChildren(context, TextModes.DATA, []),/* 解析子节点 */
getSelection(context, start)
)
}
parseChildren 生成的子节点赋值于根节点的 children 属性
解析子节点
子节点会以深度优先遍历的形式进行解析,详情可查看源码:
function parseChildren(
context: ParserContext,
mode: TextModes,
ancestors: ElementNode[]
): TemplateChildNode[] {
// 当前等解析节点的父节点
const parent = last(ancestors)
// 返回的子节点
const nodes: TemplateChildNode[] = []
// isEnd 判断 `context.source` 的起始字符串是否是结束字符串
// 例如 '</',还会匹配 ancestors 的标签名
while (!isEnd(context, mode, ancestors)) {
// 待解析字符串
const s = context.source
// 当前解析的节点值
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
// 判断当前起始字符是否是插值的开始字符
if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
// 插值节点
node = parseInterpolation(context, mode)
} else if (mode === TextModes.DATA && s[0] === '<') {
// 根绝 HTML 规范处理:https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
if (s.length === 1) {
/* error */
}
/* 忽略其他情况 */
else if (/[a-z]/i.test(s[1])) {
// 元素标签,解析元素
node = parseElement(context, ancestors)
}/* 错误校验 */
}
// 当前节点既不是元素,也不是插值
if (!node) {/* 普通文本 */
node = parseText(context, mode)
}
/* 将 node 添加到 nodes 数组。如果 node 是数组的情况会下,会遍历进行添加 */
}
/* Whitespace handling strategy like v2 */
return removedWhitespace ? nodes.filter(Boolean) : nodes
}
parseChildren 函数会解析起始字符串的 tag 是否与父节点的标签名(TextModes.DATA 模式下)相同,用于判断当前的所有子节点解析完成;解析完后将节点添加到 nodes;最后根据策略是否过滤空的节点
解析文本信息
parseText 用于解析普通文本节点:
function parseText(context: ParserContext, mode: TextModes): TextNode {
// 表示文本结束的 token
const endTokens =
mode === TextModes.CDATA ? [']]>'] : ['<', context.options.delimiters[0]]
// 假设待解析字符串都是文本,不存在标签,所以索引为整个字符串长度
let endIndex = context.source.length
// 例如:someText{{...}}</tag> 或者 someText</tag>{{val}}
for (let i = 0; i < endTokens.length; i++) {
// 查找最先遇到的结束 token 的位置
const index = context.source.indexOf(endTokens[i], 1)
// 当前文本正确的结束下标
if (index !== -1 && endIndex > index) {
endIndex = index
}
}
// 解析文本内容
const content = parseTextData(context, endIndex, mode)
return {
type: NodeTypes.TEXT,
content,
loc: getSelection(context, start)
}
}
// 不仅用于 parseText,涉及文本都涉及此函数
// 解析文本的时候同时步进 context.source
function parseTextData(
context: ParserContext,
length: number,
mode: TextModes
): string {
// 获取文本
const rawText = context.source.slice(0, length)
/* 步进 context.source,移除文本内容 */
if (
mode === TextModes.RAWTEXT ||
mode === TextModes.CDATA ||
rawText.indexOf('&') === -1
) {
return rawText
} else {
// DATA or RCDATA containing "&"". Entity decoding required.
// 例如 %gt 转译为 >
return context.options.decodeEntities(
rawText,
mode === TextModes.ATTRIBUTE_VALUE
)
}
}
parseText 会根据 "分割字符" 提取纯文本作为单独的一个节点
解析插值
当起始字符串与 context.options.delimiters[0] 匹配时,说明后续内容可能是插值类型的节点,会调用此函数生成节点对象:
function parseInterpolation(
context: ParserContext,
mode: TextModes
): InterpolationNode | undefined {
// 获取插值的 开始 和 结束 的字符串。默认 ‘{{’ 和 ‘}}’
const [open, close] = context.options.delimiters
// 结束字符串最近的索引
const closeIndex = context.source.indexOf(close, open.length)
/* 步进 context.source,移除 open 字符 */
// 插值内容节点的 开始 与 结束 位置
const innerStart = getCursor(context)
const innerEnd = getCursor(context)
// 插值内容节点的长度
const rawContentLength = closeIndex - open.length
// 解析文本内容
const preTrimContent = parseTextData(context, rawContentLength, mode)
/* 步进 context.source,移除 close 字符 */
// 返回插值节点
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
constType: ConstantTypes.NOT_CONSTANT,
content,
loc: getSelection(context, innerStart, innerEnd)
},
loc: getSelection(context, start)
}
}
//demo
baseParse('{{ x }}')
/* 生成的节点对象(省略其他节点)
{
type: 5, // NodeTypes.INTERPOLATION
content: {
type: 4, // NodeTypes.SIMPLE_EXPRESSION
isStatic: false,
constType: 0, // ConstantTypes.NOT_CONSTANT
content: "x",
loc: {},
},
loc: {},
}
*/
函数所作的事情是对 {{/* ... */}} 字符串进行解析生成相应的节点对象(假设开始符号是 '{{',结束符号是 '}}')
函数首先通过 context.options.delimiters 获取开始符号与结束符号;然后从源字符串中提取 开始 与 结束 中的字符串进行处理;最后生成相应的节点对象
解析元素
parseElement 函数会解析整个元素,包括子元素:
function parseElement(
context: ParserContext,
ancestors: ElementNode[]
): ElementNode | undefined {
// 父节点
const parent = last(ancestors)
// 解析元素标签及属性,生成元素节点
const element = parseTag(context, TagType.Start, parent)
// 没有子节点的元素直接返回
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
/* ... */
return element
}
// 当前元素作为子元素的父元素
ancestors.push(element)
// 解析子元素生成节点集合
const children = parseChildren(context, mode, ancestors)
// 恢复
ancestors.pop()
// 赋值
element.children = children
if (startsWithEndTagOpen(context.source, element.tag)) {
// 目的是步进 context.source
parseTag(context, TagType.End, parent)
} else {
/* 提示 X_MISSING_END_TAG */
}
return element
}
解析标签
// 解析标签,既可用于开始标签,亦可用于结束标签
function parseTag(
context: ParserContext,
type: TagType.Start,
parent: ElementNode | undefined
): ElementNode
function parseTag(
context: ParserContext,
type: TagType.End,
parent: ElementNode | undefined
): void
function parseTag(
context: ParserContext,
type: TagType,
parent: ElementNode | undefined
): ElementNode | undefined {
/**
* 获取标签名
*
* 效果:
* 对于 <tag a b c></tag> 结果为 ['<tag', 'tag']
* 对于 </tag> 结果为 ['</tag', 'tag']
*/
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
const tag = match[1]
const ns = context.options.getNamespace(tag, parent)
/* 步进 context.source,移除match[0] */
/* 步进 context.source,移除多余空白字符 */
// 当前标签是否是 <pre> 标签,可自定义
if (context.options.isPreTag(tag)) {
context.inPre = true
}
// 解析当前标签的所有属性
let props = parseAttributes(context, type)
// 指令属性是否存在 v-pre
if (
type === TagType.Start &&
!context.inVPre &&
props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
) {
context.inVPre = true
/* context.source 重置 */
// 重新解析属性,而此时会将指令属性节点都转换为普通属性节点
props = parseAttributes(context, type).filter(p => p.name !== 'v-pre')
}
// Tag close.
let isSelfClosing = false
if (context.source.length === 0) {
/* 提示 EOF_IN_TAG */
} else {
isSelfClosing = startsWith(context.source, '/>')
/* 步进 context.source,根据 isSelfClosing 移除 /> 或者 > */
}
// 结束标签做完步进 context.source 就可以了
if (type === TagType.End) {
return
}
// 标签类型: ELEMENT/SLOT/TEMPLATE/COMPONENT
let tagType = ElementTypes.ELEMENT
// v-pre 将按照原始元素生成
if (!context.inVPre) {
if (tag === 'slot') {
tagType = ElementTypes.SLOT
} else if (tag === 'template') {
if (
props.some(
p =>
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
)
) {
// isSpecialTemplateDirective: if,else,else-if,for,slot
tagType = ElementTypes.TEMPLATE
}
} else if (isComponent(tag, props, context)) {
/**
* isComponent 函数将会尝试调用以下函数
* context.options.isCustomElement
* context.options.isBuiltInComponent
* context.options.isNativeTag
* 还会查找 is 属性, is="vue:" / v-is
*/
tagType = ElementTypes.COMPONENT
}
}
return {
type: NodeTypes.ELEMENT,
ns,
tag,
tagType,
props,
isSelfClosing,
children: [],
loc: getSelection(context, start),
codegenNode: undefined
}
}
解析元素属性
元素属性存在零个及以上的情况,元素属性也可以分成 指令属性 和 普通的属性。在未到达标签结束符号的时候,循环解析每一个属性:
function parseAttributes(
context: ParserContext,
type: TagType
): (AttributeNode | DirectiveNode)[] {
// 属性节点集合
const props = []
// 属性名集合
const attributeNames = new Set<string>()
// 如果字符串不为空且起始位置没有匹配到 "闭合" 符号,此时 <tag "当前位置" >
while (
context.source.length > 0 &&
!startsWith(context.source, '>') &&
!startsWith(context.source, '/>') /* 自闭合 */
) {
if (startsWith(context.source, '/')) {
/* 提示 UNEXPECTED_SOLIDUS_IN_TAG */
/* 步进 context.source,移除 '/' 及余下空白字符 */
continue
}
// 解析单个属性
const attr = parseAttribute(context, attributeNames)
// Trim whitespace between class
// https://github.com/vuejs/vue-next/issues/4251
/* 处理 class 属性 */
// 只有开始的标签属性才会录入
if (type === TagType.Start) {
props.push(attr)
}
/* 步进 context.source,移除多余空白字符 */
}
return props
}
parseAttributes 函数只会在 type === TagType.Start 即开始标签时返回当前节点的属性集合,是因为该函数也通用适用于结束标签,用于步进 context.source
解析单个元素属性
元素属性节点分为 指令属性节点 和 普通属性节点,区别就在于属性的名字是否以特定字符开始:
function parseAttribute(
context: ParserContext,
nameSet: Set<string>
): AttributeNode | DirectiveNode {
// 获取属性名,例如 a="b" 得到 a
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
const name = match[0]
/* 如果属性名已经存在时会提示 DUPLICATE_ATTRIBUTE */
nameSet.add(name)
/* 步进 context.source,移除属性名 */
// 属性值
let value: AttributeValue = undefined
// 如果是有值属性(a="b")
if (/^[\t\r\n\f ]*=/.test(context.source)) {
// ' = "value" ' -> '"value" '
/* 步进 context.source,移除多余空白字符 */
/* 步进 context.source,移除=*/
/* 步进 context.source,移除多余空白字符 */
value = parseAttributeValue(context)
}
// 所处环境不是 v-pre 且当前属性名与指令正则匹配,则此节点属于指令属性节点
if (!context.inVPre && /^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name)) {
/**
* 效果:
* 'v-a' -> ['v-a', 'a']
* 'v-a:b' -> ['v-a:b', 'a', 'b']
* 'v-a:b.c' -> ['v-a:b', 'a', 'b', '.c']
*
* '@a' -> ['@a', undefined, 'a']
* '@a.b' -> ['@a.b', undefined, 'a', '.b']
*
* ':a' -> [':a', undefined, 'a']
* ':a.b' -> [':a.b', undefined, 'a', '.b']
*
* '#a' -> ['#a', undefined, 'a']
* '#a.b' -> ['#a.b', undefined, 'a', '.b']
*
* '.a' -> ['.a', undefined, 'a']
* '.a.b' -> ['.a.b', undefined, 'a', '.b']
*/
const match =
/(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
name
)!
let isPropShorthand = startsWith(name, '.')
// 指令名
let dirName =
match[1] ||
(
isPropShorthand || startsWith(name, ':')
? 'bind' /* 数据绑定 */
: startsWith(name, '@')
? 'on' /* 监听 */
: 'slot' /* 插槽 */
)
// 指令参数
let arg: ExpressionNode | undefined
if (match[2]) {
const isSlot = dirName === 'slot'
// 参数值
let content = match[2]
// 表示当前获取的引用是否时静态的
let isStatic = true
// v-on:[event]="method" -> content === 'event'
// v-on:event="method" -> content === 'event'
if (content.startsWith('[')) {
isStatic = false
// 更新 content 值
if (!content.endsWith(']')) {
/* 提示 X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END */
content = content.slice(1)
} else {
content = content.slice(1, content.length - 1)
}
} else if (isSlot) {
// #1241 special case for v-slot: vuetify relies extensively on slot
// names containing dots. v-slot doesn't have any modifiers and Vue 2.x
// supports such usage so we are keeping it consistent with 2.x.
content += match[3] || ''
}
arg = {
type: NodeTypes.SIMPLE_EXPRESSION,
content,
isStatic,
constType: isStatic
? ConstantTypes.CAN_STRINGIFY
: ConstantTypes.NOT_CONSTANT,
loc
}
}
/* 如果属性值存在引号,更新属性值的 loc,生成代码时避免转换为字符串值 */
// [,,, '.a.b.c'] -> ['a', 'b', 'c']
const modifiers = match[3] ? match[3].slice(1).split('.') : []
if (isPropShorthand) modifiers.push('prop')
// 返回指令属性节点
return {
type: NodeTypes.DIRECTIVE,
name: dirName,
exp: value && {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
constType: ConstantTypes.NOT_CONSTANT,
loc: value.loc
},
arg,
modifiers,
loc
}
}
// 返回普通的属性节点
return {
type: NodeTypes.ATTRIBUTE,
name,
value: value && {
type: NodeTypes.TEXT,
content: value.content,
loc: value.loc
},
loc
}
}
//demo
baseParse('<tag id="app"></tag>')
/* 生成的节点对象(省略其他节点)
{
type: 6, // NodeTypes.ATTRIBUTE
name: "id",
value: {
type: 2, // NodeTypes.TEXT
content: "app",
loc: {},
},
loc: {},
}
*/
baseParse('<tag @click="method"></tag>')
/* 生成的节点对象(省略其他节点)
{
type: 7, // NodeTypes.DIRECTIVE
name: "on",
exp: {
type: 4, // NodeTypes.SIMPLE_EXPRESSION
content: "method",
isStatic: false,
constType: 0, // ConstantTypes.NOT_CONSTANT
loc: {},
},
arg: {
type: 4, // NodeTypes.SIMPLE_EXPRESSION
content: "click",
isStatic: true,
constType: 3, // ConstantTypes.CAN_STRINGIFY
loc: {},
},
modifiers: [],
loc: {},
}
*/
解析元素属性值
对于元素属性值的解析非常简单,就是获取引号里的文本内容(支持无引号):
function parseAttributeValue(context: ParserContext): AttributeValue {
// 属性值
let content: string
// "value" 或者 'value' 表示存在引号
const quote = context.source[0]
// isQuoted 用于指令属性节点,判断是否需要更新属性值节点的 loc
const isQuoted = quote === `"` || quote === `'`
// 获取属性值内容
if (isQuoted) {
/* 步进 context.source,移除一个引号 */
const endIndex = context.source.indexOf(quote)
if (endIndex === -1) {/* 解析剩余字符串,获得属性值 */
content = parseTextData(
context,
context.source.length,
TextModes.ATTRIBUTE_VALUE
)
} else {/* 解析特定长度的字符串,获取属性值 */
content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE)
/* 步进 context.source,移除一个引号 */
}
} else {
/* 匹配获取属性值 /^[^\t\r\n\f >]+/.exec(context.source) */
}
return { content, isQuoted, loc: getSelection(context, start) }
}
总结
baseParse 函数使用 parseChildren 函数解析模板字符串生成子节点集合,传递给根节点,最后返回抽象语法树
生成的 ast 包含了源码的所有信息;之后经过 transform 对 ast 进行转换处理,对特定节点进行特殊转换;最终生成渲染函数