🧩 函数 1:onText(content, start, end)
arduino
function onText(content: string, start: number, end: number) {
if (__BROWSER__) {
const tag = stack[0] && stack[0].tag
if (tag !== 'script' && tag !== 'style' && content.includes('&')) {
content = currentOptions.decodeEntities!(content, false)
}
}
const parent = stack[0] || currentRoot
const lastNode = parent.children[parent.children.length - 1]
if (lastNode && lastNode.type === NodeTypes.TEXT) {
// merge
lastNode.content += content
setLocEnd(lastNode.loc, end)
} else {
parent.children.push({
type: NodeTypes.TEXT,
content,
loc: getLoc(start, end),
})
}
}
📖 功能说明
当模板中出现普通文本或插值内容时(如 "Hello" 或 {{ msg }}),由 tokenizer 触发 ontext 事件,这个函数就会被调用,用于创建或合并文本节点。
🔍 拆解步骤
| 步骤 | 描述 |
|---|---|
| ① | 如果运行在浏览器端,并且内容中包含 HTML 实体(如 <),则调用 decodeEntities 进行解码。 |
| ② | 找出当前节点的父级(stack[0] 表示当前打开的标签节点,否则为根节点)。 |
| ③ | 检查上一个子节点是否也是文本节点,如果是则合并文本内容。 |
| ④ | 否则新建一个 TEXT 类型节点,推入 children 数组。 |
🧠 原理
Vue 编译器会尽量合并相邻文本节点,以减少虚拟 DOM 节点数量。例如:
css
<div>hello {{ name }}</div>
将被解析为单一文本节点:
scss
TEXT: "hello " + INTERPOLATION("name")
📌 小结
✅ 解码 HTML 实体
✅ 合并连续文本
✅ 保留精确位置(getLoc())
🧩 函数 2:endOpenTag(end)
scss
function endOpenTag(end: number) {
if (tokenizer.inSFCRoot) {
currentOpenTag!.innerLoc = getLoc(end + 1, end + 1)
}
addNode(currentOpenTag!)
const { tag, ns } = currentOpenTag!
if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
inPre++
}
if (currentOptions.isVoidTag(tag)) {
onCloseTag(currentOpenTag!, end)
} else {
stack.unshift(currentOpenTag!)
if (ns === Namespaces.SVG || ns === Namespaces.MATH_ML) {
tokenizer.inXML = true
}
}
currentOpenTag = null
}
📖 功能说明
用于处理"开始标签结束"的逻辑(如解析到 > 或 />)。
当一个标签如 <div> 解析完属性部分时,endOpenTag() 会被触发。
🔍 拆解步骤
| 步骤 | 描述 |
|---|---|
| ① | 若当前处于单文件组件 (SFC) 模式,则记录模板内层位置 (innerLoc)。 |
| ② | 调用 addNode() 将当前标签节点插入 AST。 |
| ③ | 检查是否是 <pre> 标签,若是则增加 inPre 层级(保持空格)。 |
| ④ | 若标签是自闭合(<br/>),立即关闭该节点。 |
| ⑤ | 否则将节点入栈 stack,表示成为当前上下文。 |
| ⑥ | 若命名空间为 SVG 或 MathML,则进入 XML 模式。 |
| ⑦ | 清空 currentOpenTag,准备下一个节点。 |
💡 原理
Vue 使用栈(stack)来维护嵌套标签:
css
<div>
<span></span>
</div>
解析 <div> 时入栈,解析到 </div> 时出栈。
每个节点的层级关系由 stack 决定。
🧩 函数 3:onCloseTag(el, end, isImplied = false)
ini
function onCloseTag(el: ElementNode, end: number, isImplied = false) {
if (isImplied) {
setLocEnd(el.loc, backTrack(end, CharCodes.Lt))
} else {
setLocEnd(el.loc, lookAhead(end, CharCodes.Gt) + 1)
}
...
const { tag, ns, children } = el
if (!inVPre) {
if (tag === 'slot') el.tagType = ElementTypes.SLOT
else if (isFragmentTemplate(el)) el.tagType = ElementTypes.TEMPLATE
else if (isComponent(el)) el.tagType = ElementTypes.COMPONENT
}
...
if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
inPre--
}
if (currentVPreBoundary === el) {
inVPre = tokenizer.inVPre = false
currentVPreBoundary = null
}
}
📖 功能说明
负责闭合标签的收尾工作 。
在模板解析到 </div>、</template> 等标签结束时被调用。
🔍 逻辑拆解
| 步骤 | 描述 |
|---|---|
| ① | 调整节点位置 (setLocEnd),区分隐式闭合或正常闭合。 |
| ② | 若为 SFC 模式,修正 innerLoc 的结束坐标。 |
| ③ | 根据标签内容推断节点类型(普通元素 / 模板 / 组件 / 插槽)。 |
| ④ | 压缩子节点空白符(调用 condenseWhitespace)。 |
| ⑤ | 处理 <pre> 模式与换行忽略规则。 |
| ⑥ | 若退出 v-pre 指令范围,则重置解析标志。 |
| ⑦ | 若命名空间从 SVG 返回到 HTML,则关闭 XML 模式。 |
| ⑧ | 在兼容模式下(Vue 2.x),检查 v-if、v-for、template 的语法兼容性。 |
🧠 原理讲解
Vue 解析器的闭合逻辑非常严谨,它不仅仅做结构配对,还会:
- 根据标签与属性自动推断节点语义类型;
- 检查标签未闭合、错误嵌套;
- 支持v-pre、v-if、v-for 等特殊行为恢复。
🧩 函数 4:addNode(node)
scss
function addNode(node: TemplateChildNode) {
(stack[0] || currentRoot).children.push(node)
}
📖 功能说明
将一个新节点(文本 / 注释 / 元素)追加到当前父节点的 children 中。
💡 原理
- 如果栈中有元素 → 当前节点是栈顶元素的子节点;
- 如果栈为空 → 当前节点属于根节点。
这就是 Vue AST 树的层级生成方式。
🧩 函数 5:lookAhead(index, c) & backTrack(index, c)
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: 查找闭合符号,如>;backTrack: 向后查找<起始位置(用于隐式闭合修正)。
💡 原理
基于 charCodeAt() 实现高性能字符扫描,避免正则带来的性能开销。
🧩 函数 6:condenseWhitespace(nodes)
ini
function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] {
const shouldCondense = currentOptions.whitespace !== 'preserve'
...
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === NodeTypes.TEXT) {
if (!inPre) {
if (isAllWhitespace(node.content)) {
...
} else if (shouldCondense) {
node.content = condense(node.content)
}
} else {
node.content = node.content.replace(windowsNewlineRE, '\n')
}
}
}
return removedWhitespace ? nodes.filter(Boolean) : nodes
}
📖 功能说明
用于规范化或删除节点间多余的空白符。
🔍 逻辑拆解
| 情况 | 处理方式 |
|---|---|
在 <pre> 内 |
保留原样,只统一换行符格式 |
| 含连续空格 | 压缩为单一空格 ' ' |
| 全是空白且无必要 | 删除该节点 |
模式为 'preserve' |
完全保留原样 |
🧠 设计理念
Vue 模板默认不保留无意义的空白符,提升渲染性能。
但 <pre> 等特殊标签需要忠实还原源文本。
🧩 函数 7:isAllWhitespace(str) & hasNewlineChar(str)
php
function isAllWhitespace(str: string) { ... }
function hasNewlineChar(str: string) { ... }
📖 功能说明
辅助判断函数,用于空白符检测与换行判断。
💡 应用
在 condenseWhitespace、onCloseTag 等处控制换行与文本清理逻辑。
🧩 函数 8: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
}
📖 功能说明
压缩字符串内的连续空格,使 "a b c" → "a b c"。
💡 设计动机
保证 AST 文本节点内容简洁一致,避免模板渲染时产生额外空白。
✅ 小结
至此,我们已讲解完:
- 节点创建与闭合(onText、onCloseTag、endOpenTag)
- 节点插入(addNode)
- 空白处理与辅助扫描函数
这些函数是 baseParse 的"结构层"核心,负责构建出 Vue 的 AST 形状。
📘 下一章预告
第三章我们将进入更复杂的解析逻辑:
🔹 指令解析(
ondirname、ondirarg、ondirmodifier)🔹 属性与值解析(
onattribdata、onattribend)🔹 插值解析(
oninterpolation)
这些函数涉及 Vue 模板语法的灵魂部分(v-if、v-for、v-bind、{{ }} 等)。