Appearance
语法分析
词法分析产生了扁平的 Token 序列,但模板是有层次结构的——元素可以嵌套。如何将 Token 序列转换为树形结构?
这就是语法分析的工作。本章将分析 Vue 3 的递归下降解析器。 递归下降是构建解析器最直观、最易理解的方法。
输入与输出
javascript
// 输入:模板字符串
const template = `
<div class="container">
<p v-if="show">{{ message }}</p>
<button @click="handleClick">Click</button>
</div>
`
// 输出:AST(抽象语法树)
{
type: NodeTypes.ROOT,
children: [{
type: NodeTypes.ELEMENT,
tag: 'div',
props: [{
type: NodeTypes.ATTRIBUTE,
name: 'class',
value: { content: 'container' }
}],
children: [
{
type: NodeTypes.ELEMENT,
tag: 'p',
props: [{
type: NodeTypes.DIRECTIVE,
name: 'if',
exp: { content: 'show' }
}],
children: [{
type: NodeTypes.INTERPOLATION,
content: { content: 'message' }
}]
},
{
type: NodeTypes.ELEMENT,
tag: 'button',
props: [{
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: { content: 'click' },
exp: { content: 'handleClick' }
}],
children: [{
type: NodeTypes.TEXT,
content: 'Click'
}]
}
]
}]
}递归下降算法
Vue 3 使用递归下降(Recursive Descent)解析算法。核心思想:从根节点开始,递归处理子节点。
javascript
function parse(template) {
const context = createParserContext(template)
return createRoot(parseChildren(context, []))
}
function createRoot(children) {
return {
type: NodeTypes.ROOT,
children,
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
codegenNode: undefined
}
}parseChildren:核心循环
javascript
function parseChildren(context, ancestors) {
const nodes = []
while (!isEnd(context, ancestors)) {
const s = context.source
let node = undefined
if (s.startsWith('\u007b\u007b')) {
// 插值表达式
node = parseInterpolation(context)
} else if (s[0] === '<') {
if (s[1] === '!') {
if (s.startsWith('<!--')) {
// 注释
node = parseComment(context)
} else if (s.startsWith('<!DOCTYPE')) {
// DOCTYPE,忽略
parseBogusComment(context)
}
} else if (s[1] === '/') {
// 结束标签
if (/[a-z]/i.test(s[2])) {
parseTag(context, TagType.End)
continue
}
} else if (/[a-z]/i.test(s[1])) {
// 开始标签
node = parseElement(context, ancestors)
}
}
// 默认作为文本处理
if (!node) {
node = parseText(context)
}
if (node) {
nodes.push(node)
}
}
return nodes
}判断逻辑:
{{→ 插值表达式<!--→ 注释</→ 结束标签(不产生节点)<字母→ 元素- 其他 → 文本
parseElement:解析元素
javascript
function parseElement(context, ancestors) {
// 1. 解析开始标签
const element = parseTag(context, TagType.Start)
// 自闭合标签,直接返回
if (element.isSelfClosing || isVoidTag(element.tag)) {
return element
}
// 2. 递归解析子节点
ancestors.push(element)
const children = parseChildren(context, ancestors)
ancestors.pop()
element.children = children
// 3. 解析结束标签
if (context.source.startsWith(`</${element.tag}`)) {
parseTag(context, TagType.End)
} else {
emitError(context, ErrorCodes.X_MISSING_END_TAG)
}
return element
}ancestors 栈的作用:
- 追踪当前的父元素链
- 判断结束标签是否匹配
- 检测未闭合的标签
parseTag:解析标签
javascript
function parseTag(context, type) {
const start = getCursor(context)
// 匹配标签名
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)
const tag = match[1]
advanceBy(context, match[0].length)
advanceSpaces(context)
// 解析属性
const props = parseAttributes(context)
// 检查自闭合
let isSelfClosing = false
if (context.source.startsWith('/>')) {
isSelfClosing = true
advanceBy(context, 2)
} else {
advanceBy(context, 1) // >
}
// 判断元素类型
let tagType = ElementTypes.ELEMENT
if (tag === 'slot') {
tagType = ElementTypes.SLOT
} else if (tag === 'template') {
tagType = ElementTypes.TEMPLATE
} else if (isComponent(tag, context)) {
tagType = ElementTypes.COMPONENT
}
return {
type: NodeTypes.ELEMENT,
tag,
tagType,
props,
isSelfClosing,
children: [],
loc: getSelection(context, start)
}
}parseAttributes:解析属性
javascript
function parseAttributes(context) {
const props = []
const attributeNames = new Set()
while (
context.source.length > 0 &&
!context.source.startsWith('>') &&
!context.source.startsWith('/>')
) {
const attr = parseAttribute(context, attributeNames)
if (attr) {
props.push(attr)
}
advanceSpaces(context)
}
return props
}
function parseAttribute(context, nameSet) {
const start = getCursor(context)
// 匹配属性名
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)
const name = match[0]
// 检查重复
if (nameSet.has(name)) {
emitError(context, ErrorCodes.DUPLICATE_ATTRIBUTE)
}
nameSet.add(name)
advanceBy(context, name.length)
// 解析值
let value = undefined
if (context.source[0] === '=') {
advanceBy(context, 1)
advanceSpaces(context)
value = parseAttributeValue(context)
}
// 检查是否是指令
if (/^(v-|:|@|#)/.test(name)) {
return parseDirective(name, value, start, context)
}
return {
type: NodeTypes.ATTRIBUTE,
name,
value: value && {
type: NodeTypes.TEXT,
content: value.content,
loc: value.loc
},
loc: getSelection(context, start)
}
}isEnd:判断结束条件
javascript
function isEnd(context, ancestors) {
const s = context.source
// 模板解析完毕
if (!s) {
return true
}
// 遇到父元素的结束标签
for (let i = ancestors.length - 1; i >= 0; i--) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true
}
}
return false
}
function startsWithEndTagOpen(source, tag) {
return (
source.startsWith('</') &&
source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase() &&
/[\t\r\n\f />]/.test(source[2 + tag.length] || '>')
)
}错误恢复
遇到错误时,解析器会尝试恢复继续:
javascript
function parseChildren(context, ancestors) {
const nodes = []
while (!isEnd(context, ancestors)) {
try {
// 正常解析...
} catch (e) {
// 错误恢复:跳过当前字符
emitError(context, e.code)
advanceBy(context, 1)
}
}
return nodes
}位置信息
每个节点都携带位置信息,用于错误提示:
javascript
{
type: NodeTypes.ELEMENT,
tag: 'div',
loc: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 3, column: 7, offset: 42 },
source: '<div>...</div>'
}
}本章小结
本章分析了语法分析的实现:
- 递归下降:从根开始,递归处理子节点
- parseChildren:核心循环,根据首字符判断类型
- parseElement:解析元素,包括开始标签、子节点、结束标签
- ancestors 栈:追踪父元素链,判断结束条件
- 错误恢复:遇到错误跳过继续解析
- 位置信息:记录每个节点的源码位置
语法分析将扁平的字符流转换为层次化的 AST,为后续的转换和代码生成奠定基础。
下一章,我们将详细分析 AST 节点的类型设计。