🧩 函数 1:condenseWhitespace(nodes)
ini
function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] {
const shouldCondense = currentOptions.whitespace !== 'preserve'
let removedWhitespace = false
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === NodeTypes.TEXT) {
if (!inPre) {
if (isAllWhitespace(node.content)) {
const prev = nodes[i - 1] && nodes[i - 1].type
const next = nodes[i + 1] && nodes[i + 1].type
if (
!prev ||
!next ||
(shouldCondense &&
((prev === NodeTypes.COMMENT &&
(next === NodeTypes.COMMENT || next === NodeTypes.ELEMENT)) ||
(prev === NodeTypes.ELEMENT &&
(next === NodeTypes.COMMENT ||
(next === NodeTypes.ELEMENT &&
hasNewlineChar(node.content))))))
) {
removedWhitespace = true
nodes[i] = null as any
} else {
node.content = ' '
}
} else if (shouldCondense) {
node.content = condense(node.content)
}
} else {
node.content = node.content.replace(windowsNewlineRE, '\n')
}
}
}
return removedWhitespace ? nodes.filter(Boolean) : nodes
}
📖 功能说明
统一处理节点中的空白字符(Whitespace),用于压缩模板文本、优化渲染性能。
🔍 逻辑拆解
| 场景 | 处理方式 |
|---|---|
在 <pre> 内部 |
保留原样,只将 \r\n 转换为 \n。 |
| 空白节点(全是空格或换行) | 可能被删除或压缩成一个空格 ' '。 |
| 普通文本中的多余空格 | 压缩为单空格(调用 condense())。 |
| 空白在元素/注释之间 | 若 whitespace: "condense" 且存在换行符,则删除节点。 |
🧠 设计理念
Vue 默认模板渲染不保留多余空格,这是性能优化与 HTML 渲染一致性考虑。
但保留 <pre> 标签原样以尊重语义。
📘 举例
css
<div>
Hello
World
</div>
压缩后等价为:
css
[ { "type": "TEXT", "content": " Hello World " }]
🧩 函数 2:condense(str)
ini
function condense(str: string) {
let ret = ''
let prevCharIsWhitespace = false
for (let i = 0; i < str.length; i++) {
if (isWhitespace(str.charCodeAt(i))) {
if (!prevCharIsWhitespace) {
ret += ' '
prevCharIsWhitespace = true
}
} else {
ret += str[i]
prevCharIsWhitespace = false
}
}
return ret
}
📖 功能说明
将字符串中连续空白压缩为单一空格。
例如:
"foo bar baz" → "foo bar baz"
💡 原理
利用标志 prevCharIsWhitespace 记录上一个字符是否是空格,
在连续空格时仅保留第一个。
🧩 函数 3:isAllWhitespace(str) 与 hasNewlineChar(str)
rust
function isAllWhitespace(str: string) {
for (let i = 0; i < str.length; i++) {
if (!isWhitespace(str.charCodeAt(i))) {
return false
}
}
return true
}
function hasNewlineChar(str: string) {
for (let i = 0; i < str.length; i++) {
const c = str.charCodeAt(i)
if (c === CharCodes.NewLine || c === CharCodes.CarriageReturn) {
return true
}
}
return false
}
📖 功能说明
提供文本判断工具:
isAllWhitespace()→ 判断字符串是否全部为空白;hasNewlineChar()→ 判断是否包含换行符。
💡 用途
这两个函数被频繁用于:
- 空白节点删除;
condenseWhitespace()中的逻辑判断;onCloseTag()的新行处理。
🧩 函数 4:lookAhead() 与 backTrack()
css
function lookAhead(index: number, c: number) {
let i = index
while (currentInput.charCodeAt(i) !== c && i < currentInput.length - 1) i++
return i
}
function backTrack(index: number, c: number) {
let i = index
while (currentInput.charCodeAt(i) !== c && i >= 0) i--
return i
}
📖 功能说明
字符扫描工具,用于在字符串中向前或向后搜索特定字符。
📘 举例
lookAhead(end, CharCodes.Gt)用于查找下一个>的位置。backTrack(start, CharCodes.Lt)用于向后回溯到上一个<(如隐式闭合标签时)。
🧠 设计思路
相比正则表达式,这种"手动扫描"效率更高,也能在解析过程中精确追踪行列位置。
🧩 函数 5:setLocEnd() 与 getLoc()
arduino
function setLocEnd(loc: SourceLocation, end: number) {
loc.end = tokenizer.getPos(end)
loc.source = getSlice(loc.start.offset, end)
}
📖 功能说明
更新 AST 节点的结束位置信息。
在节点合并、闭合、或文本扩展时调用。
💡 原理
通过 tokenizer 的偏移量转化为源码位置信息(行号、列号、偏移量),
让 AST 的每个节点都带有可追溯的源代码定位。
🧩 函数 6:emitError()
less
function emitError(code: ErrorCodes, index: number, message?: string) {
currentOptions.onError(
createCompilerError(code, getLoc(index, index), undefined, message),
)
}
📖 功能说明
统一错误报告接口。
🔍 特点
- 使用
ErrorCodes枚举控制错误类型; - 自动生成位置信息;
- 调用
onError回调(默认打印到控制台)。
📘 常见错误码
| 错误码 | 描述 |
|---|---|
X_MISSING_END_TAG |
缺少闭合标签 |
X_INVALID_EXPRESSION |
表达式语法错误 |
EOF_IN_TAG |
解析到标签结尾前遇到文件结束 |
🧩 函数 7:isFragmentTemplate() 与 isComponent()
这些函数我们在上一章讲过,但这里补充一点运行流程背景:
isFragmentTemplate()在onCloseTag()时判断<template>是否包裹逻辑性指令(v-if/v-for)。isComponent()在同一位置判断标签是否是组件(根据首字母大写、:is动态绑定等规则)。
这两个函数是 节点语义分类的最后一步 。
Vue 编译器正是在这里决定每个标签的"生成代码类型":
是组件、模板、插槽、还是普通元素。
🧩 函数 8:baseParse() 全流程整合(最终回顾)
scss
export function baseParse(input: string, options?: ParserOptions): RootNode {
reset() // ① 清理状态
currentInput = input
currentOptions = extend({}, defaultParserOptions)
if (options) Object.assign(currentOptions, options)
tokenizer.mode = ... // ② 选择模式 (HTML / SFC / Base)
tokenizer.inXML = ...
const delimiters = options?.delimiters
if (delimiters) {
tokenizer.delimiterOpen = toCharCodes(delimiters[0])
tokenizer.delimiterClose = toCharCodes(delimiters[1])
}
const root = (currentRoot = createRoot([], input)) // ③ 创建 Root AST
tokenizer.parse(currentInput) // ④ 启动状态机解析
root.loc = getLoc(0, input.length)
root.children = condenseWhitespace(root.children)
currentRoot = null
return root // ⑤ 返回 AST
}
🔍 全流程图解
scss
模板字符串
↓
[Tokenizer]
├── onopentagname() → ElementNode
├── onattribname() / ondirname() → Attribute / DirectiveNode
├── ontext() → TextNode
├── oninterpolation() → InterpolationNode
├── onclosetag() → 栈出栈,结构闭合
↓
[AST 树生成完毕]
↓
condenseWhitespace() → 空白优化
↓
RootNode 返回
⚙️ Vue 解析器核心架构总结
| 模块 | 主要职责 | 示例 |
|---|---|---|
| Tokenizer | 词法分析:识别标签、文本、插值 | <div>{{ msg }}</div> |
| Directive Parser | 语义解析:识别指令、参数、修饰符 | v-if="ok" @click.stop |
| Expression Parser | 语法分析:用 Babel 构建表达式 AST | "ok && show" |
| Tree Builder | 维护节点栈,生成层级关系 | <div><span></span></div> |
| Whitespace Optimizer | 清理空白与冗余节点 | condenseWhitespace() |
| Error Reporter | 统一错误系统与位置追踪 | emitError() |
💡 运行原理简述
Vue 的模板解析器采用事件驱动状态机 + 栈式构建模型:
- Tokenizer 负责扫描字符流;
- Parser 监听事件(如
onopentagname、ontext); - Builder 负责维护栈结构与父子关系;
- Expression 解析器 负责 Babel AST;
- Whitespace 处理器 进行语义级压缩。
整个系统具有:
- ⚙️ 流式解析(无需整句加载);
- 🔍 位置信息追踪(支持精确错误提示);
- 🧩 插件式扩展点(支持 SFC、兼容模式)。
🧾 最终总结
Vue 3 的 baseParse() 是一个兼具 编译器严谨性 与 框架灵活性 的模板解析器。
它融合了:
- HTML 状态机解析;
- Vue 特有语义规则;
- Babel 表达式分析能力;
- 严格的 AST 结构化体系。
📚 一句话总结
Vue 的模板解析器,是一台能理解"语义"的 HTML 状态机,它不仅能读懂
<div>,还能明白v-if与{{ msg }}的意图。
本文部分内容借助 AI 辅助生成,并由作者整理审核。