Vue3 Tokenizer 词法分析器完整状态机流转深度梳理(3.4+ 新版架构)
一、前置总览:Vue 模板编译器完整链路
Vue 模板编译器的核心能力是将模板字符串 转换为可执行的 render 渲染函数,整体分为三大核心阶段,层层递进完成编译:
- Parser 解析阶段:模板字符串 → 词法分析 + 语法分析 → 模板 AST
- Transform 转换阶段:模板 AST → JavaScript AST
- Generate 生成阶段:JavaScript AST → render 函数字符串
完整执行链路:
3.4+ 版本重大架构重构(核心差异)
Vue3.4 是编译器架构的分水岭,彻底移除独立 scanner.ts,统一封装为 Tokenizer 类,彻底改变词法与语法解析的执行模式:
1. Vue3.0 ~ 3.3 旧架构
- 文件:
scanner.ts(独立 Scanner)+parser.ts - 架构:词法分析、语法分析完全分离
- 流程:模板字符串 → Scanner 生成完整 Token 数组 → Parser 循环消费 Token → 构建 AST
- 特点:Token 作为独立中间载体,解析流程解耦,存在完整 Token 缓存
2. Vue3.4+ 新架构(本文基准版本:3.5.40)
- 文件:
packages/compiler-core/src/tokenizer.ts,废弃scanner.ts - 架构:词法扫描、AST 构建内置在同一个 Tokenizer 内部,深度交织
- 流程:逐字符扫描模板,识别文本、标签、插值、属性等语法单元后,立即触发回调同步创建 AST 节点
- 特点:流式执行,无完整 Token 中间数组,无需外部消费 Token,调用
tokenizer.parse()一次性完成解析,性能更优
模板编译完整示例链路
源码模板字符串:
html
<div>
<h1 :id="dynamicId">Vue Template</h1>
</div>
编译流转:模板字符串 → Parser 生成模板 AST → Transform 转换 JS AST → Generate 输出 render 函数
最终生成可执行渲染函数:
ts
function render(){
return h("div",[
h("h1",{id:dynamicId},"Vue Template")
])
}
二、Parser 入口:baseParse 整体工作流程
2.1 核心调用链路
compile() → baseParse() → tokenizer.parse()
AST 本质:结构化描述模板嵌套关系的抽象语法树,是模板字符串的结构化数据映射,包含节点类型、嵌套关系、属性、源码位置等核心信息。
可在 compiler-core/src/compile.ts 的 baseCompile 中打印 AST,直观查看完整结构:
ts
function baseCompile(source, options = {}) {
const resolvedOptions = extend({}, options, {
prefixIdentifiers
});
const ast = isString(source) ? baseParse(source, resolvedOptions) : source;
console.log("🚀 ~ baseCompile ~ ast:", ast)
return ast
}
2.2 模板 AST 核心结构示例
ts
{
type: 0, // Root 根节点
children: [
{
tag: "div",
type: 1, // Element 元素节点
props: [],
children: [
{
tag: "h1",
type: 1,
props: [
{
name: "bind",
rawName: ":id",
type: 7, // 指令节点
exp:{
type: 4, // 表达式节点
content: "dynamicId"
}
}
],
children:[
{
type: 2, // 文本节点
content: "Vue Template"
}
]
}
]
}
]
}
2.3 AST 核心特征总结
- 通过
type区分节点类型:0=根节点、1=元素节点、2=文本节点、4=表达式节点、7=指令节点 - 元素子节点统一存储在
children数组,天然对应模板嵌套结构 - 元素原生属性、Vue 指令统一存放在
props数组 - 不同节点拥有专属字段:指令节点含
name/arg/modifiers,表达式节点含content - AST 与原始模板完全同构,模板所有顶级标签均作为 Root 根节点的子元素
2.4 baseParse 完整执行流程
baseParse 整体执行流程:
核心源码逻辑:
ts
export function baseParse(input: string, options?: ParserOptions): RootNode {
// 1. 重置全局状态,防止多次解析状态污染
reset()
currentInput = input
currentOptions = extend({}, defaultParserOptions)
// 2. 合并用户自定义配置
if (options) {
let key: keyof ParserOptions
for (key in options) {
if (options[key] != null) {
currentOptions[key] = options[key]
}
}
}
// 3. 设置 Tokenizer 解析模式
tokenizer.mode = currentOptions.parseMode === 'html'
? ParseMode.HTML
: currentOptions.parseMode === 'sfc'
? ParseMode.SFC
: ParseMode.BASE
// 4. 开启 XML 严格模式(SVG/MathML 专属)
tokenizer.inXML = currentOptions.ns === Namespaces.SVG || currentOptions.ns === Namespaces.MATH_ML
// 5. 自定义插值分隔符
const delimiters = options && options.delimiters
if (delimiters) {
tokenizer.delimiterOpen = toCharCodes(delimiters[0])
tokenizer.delimiterClose = toCharCodes(delimiters[1])
}
// 6. 创建根 AST 节点
const root = (currentRoot = createRoot([], input))
// 7. 启动状态机逐字符解析,回调构建 AST
tokenizer.parse(currentInput)
// 8. 填充位置信息、优化空白节点
root.loc = getLoc(0, input.length)
root.children = condenseWhitespace(root.children)
// 9. 清空引用,防止内存泄漏
currentRoot = null
return root
}
2.5 全局核心变量设计
Vue Parser 采用模块级全局变量共享解析上下文,而非每次创建新上下文,性能更优:
reset():清空上次解析残留的游标、缓存、标签栈,必须执行,否则多模板解析会出现 AST 错乱、内容串扰currentRoot:指向当前正在构建的 AST 根节点,Tokenizer 识别语法单元后直接向其children追加节点currentInput:缓存原始模板字符串,用于计算源码行列、偏移位置
2.6 源码位置信息 loc
所有 AST 节点都携带 SourceLocation 位置信息,用于编译报错精准定位源码行列:
ts
interface SourceLocation {
start: Position // { line, column, offset }
end: Position
source?: string
}
三、Tokenizer 核心架构与源码解析
Tokenizer 基于有限状态机(FSM) 实现,核心逻辑:逐字符读取模板码点,根据当前状态执行对应逻辑,支持状态跳转、区间标记、回调抛事件,游标单向前进,无字符回退,解析性能极致。
3.1 Tokenizer 核心成员变量
ts
export default class Tokenizer {
public state: State = State.Text // 状态机核心:当前解析状态
private buffer = '' // 待解析模板字符串缓冲区
public sectionStart = 0 // 语法单元起始下标,用于截取 Token
private index = 0 // 全局扫描游标:当前处理字符下标
private entityStart = 0 // HTML 实体解析起始位置
private baseState = State.Text // 临时备份状态,用于实体解析后恢复
public inRCDATA = false // RCDATA 原始文本模式(script/style 内部)
public inXML = false // XML 严格模式(SVG/MathML 启用)
public inVPre = false // v-pre 指令作用域标记,禁用插值解析
private newlines: number[] = [] // 记录所有换行下标,快速计算行列号
private readonly entityDecoder?: EntityDecoder // Node 环境实体解码器
public mode: ParseMode = ParseMode.BASE // 解析模式:BASE/HTML/SFC
}
3.2 State 状态
state: State 整个分词器的心脏。State 是枚举,包含几十种解析状态:文本、标签名、属性名、属性值、插值、注释、CDATA、实体等等。 每一个状态对应一个 stateXxx() 处理函数。全部状态枚举分类:
ts
// 1. 基础文本 & 插值
Text, InterpolationOpen, Interpolation, InterpolationClose
// 2. 标签整体解析
BeforeTagName, InTagName, InSelfClosingTag, BeforeClosingTagName, InClosingTagName, AfterClosingTagName
// 3. 属性 + Vue指令完整子状态
BeforeAttrName, InAttrName, InDirName, InDirArg, InDirDynamicArg, InDirModifier,
AfterAttrName, BeforeAttrValue, InAttrValueDq(双引号值), InAttrValueSq(单引号值), InAttrValueNq(无引号值)
// 4. 注释、CDATA、声明、处理指令
BeforeDeclaration, InDeclaration, InProcessingInstruction,
BeforeComment, CDATASequence, InCommentLike, InSpecialComment
// 5. Script/Style/Title/Textarea 特殊RCDATA标签
BeforeSpecialS, BeforeSpecialT, SpecialStartSequence, InRCDATA
// 6. HTML实体解码
InEntity
// 7. SFC根标签特殊解析
InSFCRootTagName
3.3 关键指针区分(极易混淆)
index:全局扫描游标,持续自增,代表当前正在处理的字符下标sectionStart:语法单元起点标记,用于界定一段完整 Token(文本、标签、属性)的起始位置,解析完成后通过[sectionStart, index]截取内容
3.4 回调机制 Callbacks(核心设计思想)
Tokenizer 只负责识别语法结构 ,不直接创建 AST 节点。识别完成后触发对应回调,由外层 Parser 完成 AST 构建,实现词法扫描与 AST 构建解耦。
核心回调包含文本、插值、标签、属性、指令、注释、错误捕获等全场景:
ts
export interface Callbacks {
// 文本与实体
ontext(start: number, endIndex: number): void
ontextentity(char: string, start: number, endIndex: number): void
// 插值
oninterpolation(start: number, endIndex: number): void
// 标签
onopentagname(start: number, endIndex: number): void
onopentagend(endIndex: number): void
onselfclosingtag(endIndex: number): void
onclosetag(start: number, endIndex: number): void
// 普通属性
onattribdata(start: number, endIndex: number): void
onattribentity(char: string, start: number, end: number): void
onattribend(quote: QuoteType, endIndex: number): void
onattribname(start: number, endIndex: number): void
onattribnameend(endIndex: number): void
// Vue 指令
ondirname(start: number, endIndex: number): void
ondirarg(start: number, endIndex: number): void
ondirmodifier(start: number, endIndex: number): void
// 注释、CDATA、声明
oncomment(start: number, endIndex: number): void
oncdata(start: number, endIndex: number): void
onprocessinginstruction(start: number, endIndex: number): void
// 收尾与错误
onend(): void
onerr(code: ErrorCodes, index: number): void
}
3.5 构造函数与环境差异
ts
constructor(
private readonly stack: ElementNode[],
private readonly cbs: Callbacks,
) {
// 仅 Node 环境初始化实体解码器,浏览器环境舍弃以减小包体积
if (!__BROWSER__) {
this.entityDecoder = new EntityDecoder(htmlDecodeTree, (cp, consumed) =>
this.emitCodePoint(cp, consumed),
)
}
}
环境核心差异:
- 浏览器环境:依赖 DOM 原生 API 解码 HTML 实体,无需内置解码器,减小运行时体积
- Node/SSR/SFC 编译环境:手动解码
、< 等实体字符
3.6 入口 parse() 主驱动流程
Tokenizer 唯一入口,实现单向逐字符扫描,无回溯、无前瞻,纯状态机驱动:
ts
public parse(input: string): void {
this.buffer = input
// 逐字符遍历模板字符串
while (this.index < this.buffer.length) {
const c = this.buffer.charCodeAt(this.index)
// 记录换行位置(实体解析阶段不记录)
if (c === CharCodes.NewLine && this.state !== State.InEntity) {
this.newlines.push(this.index)
}
// 根据当前状态分发处理逻辑
switch (this.state) {
case State.Text: this.stateText(c); break
case State.InterpolationOpen: this.stateInterpolationOpen(c); break
case State.InEntity: this.stateInEntity(); break
// 省略其余状态分发
}
// 游标统一前进,杜绝字符丢失
this.index++
}
// 处理末尾残留文本
this.cleanup()
// 解析完成收尾回调
this.finish()
}
核心设计亮点 :使用 charCodeAt 数字对比替代字符串对比,性能更高;全程单向扫描,通过状态跳转替代字符回退,逻辑更严谨。
四、Tokenizer 八大状态分组与核心流转规则
Tokenizer 所有状态被划分为 8 大独立分组,覆盖模板所有语法场景,所有解析逻辑均围绕状态流转实现:
- 基础文本组 Text:默认初始状态,所有解析收尾默认回流至此
- 插值语法组 InterpolationFlow :解析
{{ expr }}插值语法 - 标签解析组 TagRootFlow :处理所有以
<开头的标签场景 - 属性&指令组 AttrDirectiveFlow:解析原生属性 + Vue 全套指令语法(最复杂)
- 注释&声明组 DeclarationCommentFlow:解析注释、CDATA、DOCTYPE、处理指令
- RCDATA 原始文本组 RCDATAFlow:处理 script/style/title/textarea 特殊标签
- HTML 实体解码组 InEntity :解析
&xxx;转义字符 - SFC 根标签专属组 InSFCRootTagName:适配 .vue 文件顶层标签解析规则
主状态流转图:
4.1 全局特殊优先级规则(高频踩坑点)
-
v-pre 优先级最高 :
v-pre作用域内,禁用插值解析、指令解析,所有语法原样作为文本展示 -
RCDATA 双规则区分:
script/style:完全原始文本,不解析标签、不解析插值,仅匹配闭合标签title/textarea:不解析标签,但支持插值和实体解码
-
inXML 严格模式:SVG/MathML 命名空间开启,关闭 RCDATA 特殊规则,遵循 XML 严格语法
-
标签栈 stack 作用:校验标签嵌套层级、判断当前 RCDATA 状态、闭合标签时恢复解析上下文
五、核心状态流转逐段深度拆解
5.1 初始默认状态:State.Text(最高频状态)
解析器启动默认状态,绝大多数语法解析完成后都会回流至此。核心逻辑:持续累积普通文本,命中特殊符号则截断文本、切换对应解析分支。该状态是模板解析的基础兜底状态,所有空白文本、标签内普通内容均由此状态处理。
ts
private stateText(c: number): void {
// 1. 遇到 < :截断文本,进入标签解析分支
if (c === CharCodes.Lt) {
if (this.index > this.sectionStart) {
this.cbs.ontext(this.sectionStart, this.index)
}
this.state = State.BeforeTagName
this.sectionStart = this.index
}
// 2. Node 环境遇到 & :启动 HTML 实体解码
else if (!__BROWSER__ && c === CharCodes.Amp) {
this.startEntity()
}
// 3. 非 v-pre 环境遇到 { :启动插值匹配
else if (!this.inVPre && c === this.delimiterOpen[0]) {
this.state = State.InterpolationOpen
this.delimiterIndex = 0
this.stateInterpolationOpen(c)
}
}
ontext 回调:文本节点合并优化
为避免大量零散文本节点,Parser 会自动合并连续文本节点,同时区分环境处理实体解码:
ts
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) {
lastNode.content += content
setLocEnd(lastNode.loc, end)
} else {
// 新建文本节点
parent.children.push({
type: NodeTypes.TEXT,
content,
loc: getLoc(start, end),
})
}
}
案例说明:
<p>普通文本 {{ msg }} 尾部文本</p>
逐字符运行逻辑拆解:
- 解析初始默认 State.Text,持续累积「普通文本 」字符,游标 index 持续自增;
- 当 index 匹配到第一个
{字符,检测到非 v-pre 环境、命中插值起始符; - 立即截断已累积的文本内容,触发
ontext回调生成文本节点,切换状态为 InterpolationOpen,重置语法单元起始位 sectionStart; - 插值解析完成后,状态自动回流 State.Text,继续解析「 尾部文本」,直至遇到标签闭合
<符号。
5.2 插值语法完整闭环:Interpolation 三段式状态
插值采用三状态闭环 解析:InterpolationOpen(匹配{{)→ Interpolation(收集内容)→ InterpolationClose(匹配}}),完美兼容奇数 }、零散括号等边界场景,是模板动态内容解析的核心链路。
核心测试案例(含边界场景) :
<span>姓名:{{ userName + '_' + age }} 测试}单括号{{ status }}</span>
1. stateInterpolationOpen:匹配插值开头 {{
通过 delimiterIndex 序列游标,精准匹配多字符插值分隔符,匹配失败不丢字符,自动回退重处理。
案例运行逻辑:
- 文本状态解析至
{{首个{,触发插值开启状态,delimiterIndex 从 0 开始自增匹配; - 第二个
{匹配完成,判定插值开头匹配成功; - 截断前面「姓名:」文本并触发 ontext 回调,切换为内容收集状态,标记插值起始位置。
ts
private stateInterpolationOpen(c: number): void {
if (c === this.delimiterOpen[this.delimiterIndex]) {
// 完整匹配 {{ 分隔符
if (this.delimiterIndex === this.delimiterOpen.length - 1) {
const start = this.index + 1 - this.delimiterOpen.length
// 先提交前置普通文本
if (start > this.sectionStart) {
this.cbs.ontext(this.sectionStart, start)
}
// 切换状态,开始收集插值内容
this.state = State.Interpolation
this.sectionStart = start
} else {
this.delimiterIndex++
}
} else if (this.inRCDATA) {
// RCDATA 环境匹配失败,回退原始文本模式
this.state = State.InRCDATA
this.stateInRCDATA(c)
} else {
// 匹配失败,回退普通文本模式
this.state = State.Text
this.stateText(c)
}
}
2. stateInterpolation:收集插值内部表达式
极简逻辑:吞噬所有字符,仅监听 },命中后开启结尾分隔符匹配。
案例运行逻辑 :持续收集 userName + '_' + age 所有字符,忽略内部运算符号、拼接符号,仅监听闭合 } 字符,不做语法校验(语法校验交由编译后续环节处理)。
ts
private stateInterpolation(c: number): void {
if (c === this.delimiterClose[0]) {
this.state = State.InterpolationClose
this.delimiterIndex = 0
this.stateInterpolationClose(c)
}
}
3. stateInterpolationClose:匹配插值结尾 }}
核心容错:单个 } 判定为假终止,自动回退插值状态,避免解析报错。
边界场景运行逻辑:
- 案例中第一个单独
}:仅匹配单个闭合符,delimiterIndex 未走完双符匹配逻辑,判定匹配失败; - 自动回退 State.Interpolation 状态,将该
}纳入插值内容,不终止解析; - 直至匹配到完整
}},才判定插值结束,触发oninterpolation回调生成节点。
ts
private stateInterpolationClose(c: number) {
if (c === this.delimiterClose[this.delimiterIndex]) {
// 完整匹配 }},插值解析完成
if (this.delimiterIndex === this.delimiterClose.length - 1) {
this.cbs.oninterpolation(this.sectionStart, this.index + 1)
// 根据环境切换后续状态
this.state = this.inRCDATA ? State.InRCDATA : State.Text
this.sectionStart = this.index + 1
} else {
this.delimiterIndex++
}
} else {
// 匹配失败,回退插值内容解析状态
this.state = State.Interpolation
this.stateInterpolation(c)
}
}
oninterpolation 回调:生成插值 AST 节点
自动剔除插值首尾空白、解码实体字符、v-pre 降级处理,基于上述案例最终生成表达式节点 ,content 精准保留 userName + '_' + age 完整运算逻辑,单个异常 } 正常渲染为文本,无解析报错。
ts
oninterpolation(start, end) {
// v-pre:不解析插值,直接当做普通文本处理
if (inVPre) {
return onText(getSlice(start, end), start, end)
}
// 剥离前后 {{ }},裁剪首尾空白
let innerStart = start + tokenizer.delimiterOpen.length
let innerEnd = end - tokenizer.delimiterClose.length
while (isWhitespace(currentInput.charCodeAt(innerStart))) innerStart++
while (isWhitespace(currentInput.charCodeAt(innerEnd - 1))) innerEnd--
let exp = getSlice(innerStart, innerEnd)
// 表达式内部HTML实体解码
if (exp.includes('&')) {
exp = __BROWSER__ ? currentOptions.decodeEntities!(exp, false) : decodeHTML(exp)
}
// 创建插值AST节点
addNode({
type: NodeTypes.INTERPOLATION,
content: createExp(exp, false, getLoc(innerStart, innerEnd)),
loc: getLoc(start, end),
})
},
状态流转图:
5.3 标签入口中枢:BeforeTagName(< 后路由分发)
文本状态识别到 < 后,进入该状态,作为所有标签类型的总分发入口,根据下一个字符区分所有标签场景,覆盖正常标签、闭合标签、注释、特殊指令、异常模板等所有场景。
!:注释、CDATA、DOCTYPE 声明?:XML 处理指令/:闭合标签</tag>- 字母:普通标签 / SFC 根标签 / 特殊 RCDATA 标签
- 其他非法字符:退回文本模式,兼容异常模板
字符分流规则表
| 首字符 | 目标状态 | 用途 |
|---|---|---|
! |
BeforeDeclaration | 注释、CDATA、DOCTYPE |
? |
InProcessingInstruction | XML 处理指令 <?xxx?> |
/ |
BeforeClosingTagName | 闭合标签 </tag> |
s |
BeforeSpecialS | 判断<script>/<style>原始文本标签 |
t |
BeforeSpecialT | 判断<title>/<textarea>RCDATA 标签 |
| 字母 | InTagName / InSFCRootTagName | 普通开始标签 / SFC 根标签 |
| 其他符号 | Text | 非法<,当做普通文本 |
全场景测试案例:
html
<!-- 注释内容 -->
<div>普通标签</div>
</invalid>
<?xml 处理指令?>
<script>const a = 1</script>
逐案例路由分发逻辑:
<!:匹配注释/CDATA/DOCTYPE,进入 BeforeDeclaration 状态,解析注释内容;<字母d:匹配普通自定义标签,进入 InTagName 状态,收集标签名;</:匹配闭合标签,进入 BeforeClosingTagName 状态,校验标签栈;<?:匹配 XML 处理指令,进入 InProcessingInstruction 状态;<s:匹配特殊 s 开头标签,识别为 script,预开启 RCDATA 原始文本模式。
!:注释、CDATA、DOCTYPE 声明?:XML 处理指令/:闭合标签</tag>- 字母:普通标签 / SFC 根标签 / 特殊 RCDATA 标签
- 其他非法字符:退回文本模式,兼容异常模板
ts
private stateBeforeTagName(c: number): void {
if (c === CharCodes.ExclamationMark) {
this.state = State.BeforeDeclaration
this.sectionStart = this.index + 1
} else if (c === CharCodes.Questionmark) {
this.state = State.InProcessingInstruction
this.sectionStart = this.index + 1
} else if (isTagStartChar(c)) {
this.sectionStart = this.index
// 按解析模式分发标签类型
if (this.mode === ParseMode.BASE) {
this.state = State.InTagName
} else if (this.inSFCRoot) {
this.state = State.InSFCRootTagName
} else if (!this.inXML) {
if (c === 116 /* t */) {
this.state = State.BeforeSpecialT
} else {
this.state = c === 115 /* s */ ? State.BeforeSpecialS : State.InTagName
}
} else {
this.state = State.InTagName
}
} else if (c === CharCodes.Slash) {
this.state = State.BeforeClosingTagName
} else {
// 非法 < 符号,退回文本解析
this.state = State.Text
this.stateText(c)
}
}
整体流程处理图:
5.4 特殊 RCDATA 标签解析规则
SFC 根标签专属差异化规则,不同特殊标签解析逻辑完全不同,通过案例直观区分差异:
-
<template>:正常 HTML 解析模式,支持插值、标签、指令解析案例 :
<template>{{ msg }}<p>文本</p></template>→ 正常解析插值和子标签 -
<script>/<style>:强制开启 RCDATA 原始文本模式,不解析内部标签、插值案例 :
<script>{{ msg }}<p></script>→{{ msg }}和<p>全部作为原始文本,不生成任何 AST 节点 -
<title>/<textarea>:RCDATA 模式,保留插值和实体解析能力,不解析标签案例 :
<title>{{ title }}<test></title>→ 正常解析插值,<test>作为文本展示
5.5 标签名解析与节点创建
标签名持续收集,遇到空格、>、/ 终止,触发 onopentagname 创建元素 AST 节点,随后进入属性解析阶段。
stateInTagName(普通 HTML 标签名解析)
ts
private stateInTagName(c: number): void {
if (isEndOfTagSection(c)) {
this.handleTagName(c)
}
}
stateInSFCRootTagName(SFC 根标签特殊逻辑)
ts
private stateInSFCRootTagName(c: number): void {
if (isEndOfTagSection(c)) {
const tag = this.buffer.slice(this.sectionStart, this.index)
// SFC根下除 template 之外全部进入RCDATA原始文本模式
if (tag !== 'template') {
this.enterRCDATA(toCharCodes(`</` + tag), 0)
}
this.handleTagName(c)
}
}
SFC 根标签特殊处理:非 template 根标签自动预加载闭合标签序列,进入 RCDATA 模式。
handleTagName:标签名解析完成统一入口
ts
private handleTagName(c: number) {
this.cbs.onopentagname(this.sectionStart, this.index)
this.sectionStart = -1
this.state = State.BeforeAttrName
this.stateBeforeAttrName(c)
}
onopentagname 回调:生成 ELEMENT AST 节点赋值给 currentOpenTag
ts
onopentagname(start, end) {
const name = getSlice(start, end)
currentOpenTag = {
type: NodeTypes.ELEMENT,
tag: name,
ns: currentOptions.getNamespace(name, stack[0], currentOptions.ns),
tagType: ElementTypes.ELEMENT, // will be refined on tag close
props: [],
children: [],
loc: getLoc(start - 1, end),
codegenNode: undefined,
}
},
案例 :<button class="btn" />
运行逻辑:
- 命中
<进入标签分发,识别字母 b,进入标签名解析状态; - 持续收集
button字符,遇到空格终止标签名收集; - 触发
onopentagname创建 button 元素节点,跳转至属性解析状态。
5.6 标签属性入口中枢:BeforeAttrName
标签名解析完成后进入此状态,分发四种场景:标签结束、自闭合标签、异常闭合、属性解析,是标签属性体系的核心路由。
核心逻辑:识别 > 结束标签、/ 自闭合、普通属性,兼容 IDE 增量编辑的异常 </ 场景。
ts
private stateBeforeAttrName(c: number): void {
// 1. 遇见 > :标签结束
if (c === CharCodes.Gt) {
this.cbs.onopentagend(this.index)
// 根据是否处于RCDATA切换后续解析状态
if (this.inRCDATA) {
this.state = State.InRCDATA
} else {
this.state = State.Text
}
this.sectionStart = this.index + 1
// 2. 遇见 / :预判自闭合标签
} else if (c === CharCodes.Slash) {
this.state = State.InSelfClosingTag
// 开发环境/Node编译:校验 / 后必须紧跟 >,否则抛出错误
if ((__DEV__ || !__BROWSER__) && this.peek() !== CharCodes.Gt) {
this.cbs.onerr(ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG, this.index)
}
// 3. 特殊兼容:标签内部出现 </(IDE增量编辑容错逻辑)
} else if (c === CharCodes.Lt && this.peek() === CharCodes.Slash) {
this.cbs.onopentagend(this.index)
this.state = State.BeforeTagName
this.sectionStart = this.index
// 4. 非空白字符:代表属性开始
} else if (!isWhitespace(c)) {
// 属性名称不能以 = 开头,语法错误提示
if ((__DEV__ || !__BROWSER__) && c === CharCodes.Eq) {
this.cbs.onerr(
ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME,
this.index,
)
}
this.handleAttrStart(c)
}
// 剩余空白字符:持续留在 BeforeAttrName 状态,等待有效字符
}
场景案例:
<div>:遇到 >,标签结束,生成空属性元素节点;<img src="a.png" />:遇到 /,判定自闭合标签,无需匹配闭合标签;<p class="text">:遇到普通字符,进入属性解析分支。
核心逻辑:识别 > 结束标签、/ 自闭合、普通属性,兼容 IDE 增量编辑的异常 </ 场景。
5.7 标签收尾:onopentagend / endOpenTag / onCloseTag
标签读到 > 后完成初始化,结合 <h1 :id="dynamicId">Vue Template</h1> 官方案例,完整运行流程:
- 挂载 h1 元素节点到根 AST 树,存入 props 指令属性;
- 判定非 Void 自闭合标签,将标签压入栈结构,用于嵌套校验;
- 无 v-pre、SVG 模式,保留默认解析规则;
- 文本解析完成后,匹配
</h1>闭合标签,弹出标签栈,回收上下文状态; - 标记为普通 HTML 元素节点,完成整个标签解析闭环。
标签收尾:onopentagend & endOpenTag**
当解析器读到标签结束符 >,触发回调 onopentagend,进入开放标签后置处理逻辑。
ts
onopentagend(end) {
endOpenTag(end)
},
function endOpenTag(end: number) {
if (tokenizer.inSFCRoot) {
// SFC模式:为根标签初始化 innerLoc,记录标签内部内容区间
currentOpenTag!.innerLoc = getLoc(end + 1, end + 1)
}
// 将当前构建完成的元素节点加入AST树
addNode(currentOpenTag!)
const { tag, ns } = currentOpenTag!
// 标记 <pre> 嵌套层级,影响空白处理规则
if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
inPre++
}
// Void标签(自闭合原生标签:<br/> <img>)直接执行闭合逻辑,不压入栈
if (currentOptions.isVoidTag(tag)) {
onCloseTag(currentOpenTag!, end)
} else {
// 常规标签:压入元素栈,支持后续匹配闭合标签 </tag>
stack.unshift(currentOpenTag!)
// SVG / MathML 命名空间开启XML严格解析模式
if (ns === Namespaces.SVG || ns === Namespaces.MATH_ML) {
tokenizer.inXML = true
}
}
// 清空临时开放标签对象
currentOpenTag = null
}
endOpenTag 核心流程拆解
- SFC 根标签特殊位置初始化
innerLoc用来记录标签内部内容起止位置,后续onCloseTag会修正结束偏移,用于 SFC 源码映射、代码高亮、IDE 提示。 - addNode(currentOpenTag!) 将组装好标签名、属性的
ElementNode添加到父节点 children。 - pre 层级计数
inPre++<pre>内部需要保留原始空白,全局标记嵌套深度。 - Void 标签分支 HTML 规范 void 元素不需要闭合标签,直接调用
onCloseTag完成收尾,不推入栈;解析器不会等待</img>。 - 普通元素分支 节点压入栈顶;如果是 SVG/MathML,打开
inXML模式,XML 模式下标签大小写、自闭合规则更加严格。 - 清空临时变量
currentOpenTag = null,准备解析下一个标签。
标签闭合统一处理:onCloseTag
无论是 void 标签自动闭合、自闭合标签 />、或是读到 </tag>,最终都会走到 onCloseTag,完成元素节点最后的修正工作。
ts
function onCloseTag(el: ElementNode, end: number, isImplied = false) {
// 1. 修正元素节点 loc 结束位置
if (isImplied) {
// 隐式闭合(HTML自动容错关闭标签),向前回溯定位<符号
setLocEnd(el.loc, backTrack(end, CharCodes.Lt))
} else {
// 正常闭合:向后找到 > 位置,填充loc终点
setLocEnd(el.loc, lookAhead(end, CharCodes.Gt) + 1)
}
// 2. SFC根标签:修正 innerLoc(标签内部内容区间)
if (tokenizer.inSFCRoot) {
if (el.children.length) {
el.innerLoc!.end = extend({}, el.children[el.children.length - 1].loc.end)
} else {
el.innerLoc!.end = extend({}, el.innerLoc!.start)
}
el.innerLoc!.source = getSlice(
el.innerLoc!.start.offset,
el.innerLoc!.end.offset,
)
}
// 3. 区分节点类型:普通元素 / Component / <slot> / <template>
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
}
}
// 4. 空白压缩:RCDATA模式(script/style)不压缩空白
if (!tokenizer.inRCDATA) {
el.children = condenseWhitespace(children)
}
// 5. <textarea> / <pre> 规范兼容:移除内容首字符换行
if (ns === Namespaces.HTML && currentOptions.isIgnoreNewlineTag(tag)) {
const first = children[0]
if (first && first.type === NodeTypes.TEXT) {
first.content = first.content.replace(/^\r?\n/, '')
}
}
// 6. pre嵌套层级出栈
if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
inPre--
}
// 7. v-pre 边界清除:标签闭合后恢复插值解析
if (currentVPreBoundary === el) {
inVPre = tokenizer.inVPre = false
currentVPreBoundary = null
}
// 8. 离开SVG/MathML,切回HTML解析模式
if (
tokenizer.inXML &&
(stack[0] ? stack[0].ns : currentOptions.ns) === Namespaces.HTML
) {
tokenizer.inXML = false
}
}
onCloseTag 关键设计点逐条说明
-
loc 位置修正
isImplied=true:HTML 容错隐式闭合(例如<p>aaa<div>自动关闭<p>),向前回溯找到起始<;- 正常闭合:向后查找
>,保证元素完整区间包含闭合标签。
-
SFC innerLoc 完善 收集标签内部源码区间,对应
.vue文件中<style>内容</style>的内容范围,用于热更新、源码映射。 -
ElementTypes 类型标记(编译核心) 不在
v-pre内部时区分四类元素:ElementTypes.ELEMENT:普通 HTML 标签ElementTypes.COMPONENT:自定义组件(首字母大写 / 带-)ElementTypes.TEMPLATE:<template>片段标签ElementTypes.SLOT:<slot>插槽标签 后续代码生成阶段依靠tagType判断渲染逻辑。
-
空白压缩 condenseWhitespace Vue 经典特性:连续多个空白、换行压缩为单个空格;例外:
inRCDATA=true(script/style)不压缩,保留原始源码。 -
HTML 规范兼容:textarea/pre 首换行删除 HTML 标准规定:
<textarea>、<pre>标签紧跟的第一个换行符应当忽略,编译器在这里统一处理。 -
v-pre 作用域回收 当闭合标签是
v-pre所在元素,全局恢复inVPre=false,重新开启插值解析。 -
命名空间切换 inXML=false 当外层父节点回到 HTML 命名空间,关闭 SVG/MathML 严格 XML 解析规则。
完整链路总览(开放标签 → 闭合标签)
plain
BeforeAttrName 读到 >
→ onopentagend
→ endOpenTag
→ addNode 挂载元素节点
→ Void标签 → onCloseTag(直接闭合)
→ 普通标签 → 压入stack,等待 </tag>
读到 </xxx> 闭合标签
→ 弹出栈顶元素
→ onCloseTag
→ 修正loc位置
→ SFC innerLoc 完善
→ 判断组件/插槽/template类型
→ 空白压缩
→ pre、v-pre、命名空间状态回收
六、属性 & Vue 指令完整状态机(最高复杂度)
Vue 模板解析的核心难点,在于普通 HTML 属性和Vue 自定义指令的混合解析。编译器需要通过精准的状态机分支,区分两类语法,同时兼容完整的 Vue 指令语法体系:v-xxx完整指令、:绑定语法、@事件语法、.修饰符、#插槽、动态参数。
本章通过全覆盖实战案例,串联状态跳转、回调执行、节点生成全流程。
本章核心综合测试案例(覆盖所有属性/指令场景) :
html
<button
type="button"
disabled
v-click:foo.bar="handleClick"
:[dynamicAttr]="attrValue"
@click.native="clickHandler"
>按钮</button>
案例语法拆分:
type="button":标准普通双引号 HTML 属性disabled:无值布尔原生属性v-click:foo.bar="handleClick":完整指令(指令名+静态参数+修饰符):[dynamicAttr]="attrValue":动态参数绑定指令@click.native="clickHandler":事件指令+链式修饰符
6.1 核心入口:属性分支分发 handleAttrStart
模板解析读到标签属性首位字符时,会进入统一分发入口 handleAttrStart,通过首字符精准分流,将所有属性/指令划分为三大分支,杜绝语法混淆。分支规则:
v-开头:完整自定义指令(v-xxx)→ 进入指令名解析状态./:/@/#开头:指令简写语法 → 进入指令参数解析状态- 普通字母/字符开头:原生 HTML 属性 → 进入普通属性解析状态
完整源码 + 逐行解读:
ts
private handleAttrStart(c: number) {
// v- 开头:完整指令名
if (c === CharCodes.LowerV && this.peek() === CharCodes.Dash) {
this.state = State.InDirName
this.sectionStart = this.index
}
// . : @ # 指令简写
else if (
c === CharCodes.Dot || c === CharCodes.Colon ||
c === CharCodes.At || c === CharCodes.Number
) {
this.cbs.ondirname(this.index, this.index + 1)
this.state = State.InDirArg
this.sectionStart = this.index + 1
}
// 普通原生属性
else {
this.state = State.InAttrName
this.sectionStart = this.index
}
}
源码解读:
- 判断当前字符为
v且下一位是-,判定为完整指令,切换为「指令名解析状态」,记录解析起始位置; - 匹配修饰符、绑定、事件、插槽简写首字符,提前触发指令名回调,直接进入「指令参数解析状态」;
- 其余所有字符,统一判定为普通 HTML 属性,进入「属性名解析状态」。
案例分发运行逻辑:
type:普通字母开头 → 进入普通属性解析分支;v-click:v- 前缀开头 → 进入完整指令名解析分支;:[dynamicAttr]:冒号开头 → 进入动态参数指令分支;@click:@ 开头 → 进入事件指令分支;.native:点号开头 → 进入指令修饰符分支。
6.2 普通属性完整流转链路
普通属性是 Vue 模板最基础的解析分支,完整状态链路为:
该分支原生支持布尔无值属性、带引号属性、无引号属性、空白容错、重复属性校验全场景。
案例说明:
html
<input
type="text"
placeholder=请输入内容
readonly
class="box shadow hover"
>
场景包含:
type="text":双引号标准有值属性placeholder=请输入内容:无引号原生属性值readonly:无值布尔属性class:多空白特殊格式化属性
InAttrName 状态源码(属性名收集)
核心逻辑:持续收集合法属性名字符,遇到 = / > 空白 终止,拦截 " ' < 非法字符,触发语法报错。
ts
private stateInAttrName(c: number): void {
// 分支1:遇到 = 或者标签段结束符(> / /)→ 属性名读取完成
if (c === CharCodes.Eq || isEndOfTagSection(c)) {
// 回调:通知外部,属性名区间 [sectionStart, index]
this.cbs.onattribname(this.sectionStart, this.index)
this.handleAttrNameEnd(c)
}
// 分支2:开发环境/非浏览器环境校验非法字符
else if (
(__DEV__ || !__BROWSER__) &&
(c === CharCodes.DoubleQuote ||
c === CharCodes.SingleQuote ||
c === CharCodes.Lt)
) {
// 属性名内部不允许出现 " ' <,抛出语法错误
this.cbs.onerr(ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME, this.index)
}
// else:合法字符,不切换状态!继续留在 stateInAttrName,继续累积属性名字符
}
private handleAttrNameEnd(c: number): void {
this.sectionStart = this.index
this.state = State.AfterAttrName
// 回调:属性名读取结束
this.cbs.onattribnameend(this.index)
// 把当前字符 c 交给 AfterAttrName 状态处理
this.stateAfterAttrName(c)
}
ts
// 配套回调源码
onattribname(start, end) {
// 初始化普通属性AST节点
currentProp = {
type: NodeTypes.ATTRIBUTE,
name: getSlice(start, end),
nameLoc: getLoc(start, end),
value: undefined,
loc: getLoc(start),
}
},
案例运行逻辑(以 readonly 布尔属性为例) :
- 持续收集
readonly合法字符,无非法字符拦截; - 属性名读取完成后,命中空白终止符,触发
onattribname记录属性名; - 无等号、无属性值,直接判定为布尔无值属性 ,后续自动赋值
value: true。
AfterAttrName 属性名后置兜底校验
处理属性名后空白、换行、异常终止场景,标准化属性格式,避免生成脏 AST,是属性解析的兜底状态。
ts
private stateAfterAttrName(c: number): void {
// 分支1:遇到等号 = → 准备解析属性值
if (c === CharCodes.Eq) {
this.state = State.BeforeAttrValue
}
// 分支2:遇到 / 或者 > → 当前属性是【无值属性】,且标签即将结束
else if (c === CharCodes.Slash || c === CharCodes.Gt) {
this.cbs.onattribend(QuoteType.NoValue, this.sectionStart)
this.sectionStart = -1
this.state = State.BeforeAttrName
this.stateBeforeAttrName(c)
}
// 分支3:不是空白字符 → 没有等号,直接下一个属性,当前属性为无值属性
else if (!isWhitespace(c)) {
this.cbs.onattribend(QuoteType.NoValue, this.sectionStart)
this.handleAttrStart(c)
}
// else:空白字符 → 什么都不做,继续留在 AfterAttrName,持续跳过空格
}
BeforeAttrValue 状态源码(属性值预处理)
核心逻辑:跳过属性值前所有空白,根据首字符区分双引号、单引号、无引号三种解析模式。
ts
private stateBeforeAttrValue(c: number): void {
// 分支1:双引号 " → 进入双引号包裹属性值
if (c === CharCodes.DoubleQuote) {
this.state = State.InAttrValueDq
this.sectionStart = this.index + 1 // 起点跳过引号本身
}
// 分支2:单引号 ' → 进入单引号包裹属性值
else if (c === CharCodes.SingleQuote) {
this.state = State.InAttrValueSq
this.sectionStart = this.index + 1
}
// 分支3:非空白字符 → 无引号属性值
else if (!isWhitespace(c)) {
this.sectionStart = this.index
this.state = State.InAttrValueNq
this.stateInAttrValueNoQuotes(c) // **重新消费当前字符**
}
// else:空白字符,停留在 BeforeAttrValue,持续吞噬空格
}
案例运行逻辑(placeholder 无引号属性) :
- 匹配
placeholder=后,跳过末尾空白; - 首字符为普通文本,进入 InAttrValueNq 无引号模式;
- 持续收集字符,直至命中下一个空白终止属性值。
三种属性值解析状态源码+案例
双引号属性值 InAttrValueDq(标准最常用)
最常用的标准解析模式,支持 HTML 实体解析,以闭合引号为唯一终止条件。
ts
private stateInAttrValueDoubleQuotes(c: number): void {
this.handleInAttrValue(c, CharCodes.DoubleQuote)
}
private stateInAttrValueSingleQuotes(c: number): void {
this.handleInAttrValue(c, CharCodes.SingleQuote)
}
private handleInAttrValue(c: number, quote: number) {
// 分支1:遇到闭合引号,属性值解析结束
if (c === quote || (__BROWSER__ && this.fastForwardTo(quote))) {
// 回调:输出属性值文本区间
this.cbs.onattribdata(this.sectionStart, this.index)
this.sectionStart = -1
// 回调:属性值结束,通知引号类型与结束位置
this.cbs.onattribend(
quote === CharCodes.DoubleQuote ? QuoteType.Double : QuoteType.Single,
this.index + 1,
)
this.state = State.BeforeAttrName
// 分支 2:非浏览器环境,遇到 & 符号,启动 HTML 实体解析
} else if (!__BROWSER__ && c === CharCodes.Amp) {
this.startEntity()
}
}
// 回调源码
onattribdata(start, end) {
currentAttrValue += getSlice(start, end)
if (currentAttrStartIndex < 0) currentAttrStartIndex = start
currentAttrEndIndex = end
},
案例运行逻辑(type="text") :精准收集引号内 text 内容,匹配闭合双引号后触发回调,生成标准带值属性节点,自动剔除引号符号。
无引号属性值 InAttrValueNq(严格容错)
严格容错模式,禁止特殊非法字符,以空白/标签结束符为终止条件。
ts
private stateInAttrValueNoQuotes(c: number): void {
// 分支1:空白 或者 > → 属性值结束
if (isWhitespace(c) || c === CharCodes.Gt) {
// 上报属性值文本区间
this.cbs.onattribdata(this.sectionStart, this.index)
this.sectionStart = -1
// 上报属性结束,类型:Unquoted 无引号
this.cbs.onattribend(QuoteType.Unquoted, this.index)
// 切回准备解析下一个属性
this.state = State.BeforeAttrName
this.stateBeforeAttrName(c)
}
// 分支2:非法字符校验
else if (
((**DEV** || !**BROWSER**) && c === CharCodes.DoubleQuote) ||
c === CharCodes.SingleQuote ||
c === CharCodes.Lt ||
c === CharCodes.Eq ||
c === CharCodes.GraveAccent
) {
this.cbs.onerr(ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE, this.index)
}
// 分支3:非浏览器环境遇到 &,启动实体解析
else if (!**BROWSER** && c === CharCodes.Amp) {
this.startEntity ()
}
// else:合法普通字符,停留在当前状态,继续累积字符
}
onattribend 收尾回调源码(AST 最终生成)
所有属性/指令解析的最终收尾入口,负责补齐位置信息、格式化特殊属性、区分普通属性/指令节点、处理 v-pre 特殊逻辑,最终挂载到标签 AST 上。
ts
onattribend(quote, end) {
if (currentOpenTag && currentProp) {
// 1. 更新当前属性AST的结束字符偏移位置
setLocEnd(currentProp.loc, end)
if (quote !== QuoteType.NoValue) { // 属性存在值(不是仅有名称的布尔属性 <div disabled>)
// 浏览器环境:解码属性值内 HTML 实体 &xxx;
if (__BROWSER__ && currentAttrValue.includes('&')) {
currentAttrValue = currentOptions.decodeEntities!(currentAttrValue, true)
}
if (currentProp.type === NodeTypes.ATTRIBUTE) {
// ========== 普通属性 AttributeNode 分支 ==========
// class 属性:压缩连续空白字符并首尾去空格
if (currentProp!.name === 'class') {
currentAttrValue = condense(currentAttrValue).trim()
}
// 无引号属性,但属性值为空 → 抛出缺失属性值错误
if (quote === QuoteType.Unquoted && !currentAttrValue) {
emitError(ErrorCodes.MISSING_ATTRIBUTE_VALUE, end)
}
// 构建属性 value 文本节点,记录值对应的源码区间
currentProp!.value = {
type: NodeTypes.TEXT,
content: currentAttrValue,
loc: quote === QuoteType.Unquoted
? getLoc(currentAttrStartIndex, currentAttrEndIndex)
: getLoc(currentAttrStartIndex - 1, currentAttrEndIndex + 1),
}
// SFC 根 <template lang="xxx"> 非 html:切换解析器为 RCDATA 模式,避免内部内容被解析成标签
if (
tokenizer.inSFCRoot &&
currentOpenTag.tag === 'template' &&
currentProp.name === 'lang' &&
currentAttrValue &&
currentAttrValue !== 'html'
) {
tokenizer.enterRCDATA(toCharCodes(`</template`), 0)
}
} else {
// ========== 指令 DirectiveNode 分支(v-if / @click / :src) ==========
let expParseMode = ExpParseMode.Normal
if (!__BROWSER__) {
// 根据指令类型设置表达式解析模式
if (currentProp.name === 'for') {
expParseMode = ExpParseMode.Skip // v-for 后续单独解析表达式
} else if (currentProp.name === 'slot') {
expParseMode = ExpParseMode.Params // v-slot 支持参数语法
} else if (
currentProp.name === 'on' &&
currentAttrValue.includes(';')
) {
expParseMode = ExpParseMode.Statements // @click 支持多条语句
}
}
// 创建指令表达式节点 exp
currentProp.exp = createExp(
currentAttrValue,
false,
getLoc(currentAttrStartIndex, currentAttrEndIndex),
ConstantTypes.NOT_CONSTANT,
expParseMode,
)
// v-for 额外解析循环表达式,存入 forParseResult
if (currentProp.name === 'for') {
currentProp.forParseResult = parseForExpression(currentProp.exp)
}
}
// 特殊排除:v-pre 指令不加入props列表(编译器后续跳过该节点编译)
if (
currentProp.type !== NodeTypes.DIRECTIVE ||
currentProp.name !== 'pre'
) {
currentOpenTag.props.push(currentProp)
}
}
// 清空临时缓存,准备解析下一个属性
currentAttrValue = ''
currentAttrStartIndex = currentAttrEndIndex = -1
}
核心特殊逻辑:
- 自动格式化
class多空白问题,统一标准化样式类名; - 区分普通属性和指令节点,差异化生成 AST 结构;
- 针对
v-for/v-slot/@事件适配专属表达式解析模式; v-pre指令不纳入编译列表,跳过后续解析。
6.3 Vue 指令三段式标准链路
6.3.1 指令语法规则:
所有 Vue 指令统一遵循三段式结构,支持多修饰符、动态参数:
ts
v-dirname[:arg][.modifier][=value]
v-dirname[:dynamic_arg][.modifier][.modifier...]
语法拆解示例:
v-on:click.stop:指令名=on、静态参数=click、修饰符=stopv-model:[field].trim.number:指令名=model、动态参数=field、双修饰符=trim/number
指令专属状态集合:
InDirName:读取指令名InDirArg:读取指令参数InDirDynamicArg:读取动态方括号参数[xxx]InDirModifier:读取链式指令修饰符.xxx
配套回调:ondirname/ondirarg/ondirmodifier,分别在对应阶段触发,生成指令 AST 数据。
6.3.2 各状态源码+流转逻辑解析
stateInDirName(状态:读取指令名 dirname)
核心逻辑 :根据后置字符,自动分流「结束解析/进入参数/进入修饰符」。读到 = / 标签结束 → 指令结束; 读到 : → 进入参数; 读到 . → 进入修饰符; 普通字符继续累积指令名。
ts
private stateInDirName(c: number): void {
// 情况1:遇到 = 或者标签结束符(空格/>等)
if (c === CharCodes.Eq || isEndOfTagSection(c)) {
// 截取 [sectionStart, index) 作为指令名
this.cbs.ondirname(this.sectionStart, this.index)
// 属性名称解析结束,进入属性值解析流程
this.handleAttrNameEnd(c)
}
// 情况2:遇到冒号 : → 指令参数开始 v-name:arg
else if (c === CharCodes.Colon) {
this.cbs.ondirname(this.sectionStart, this.index)
this.state = State.InDirArg
this.sectionStart = this.index + 1 // 参数起始位置跳到 : 的下一位
}
// 情况3:遇到点 . → 直接跟修饰符 v-name.modifier
else if (c === CharCodes.Dot) {
this.cbs.ondirname(this.sectionStart, this.index)
this.state = State.InDirModifier
this.sectionStart = this.index + 1 // 修饰符起始跳到 . 的下一位
}
// 其他字符:继续留在 InDirName,持续读取指令名字符,无操作
}
stateInDirArg(状态:读取普通指令参数 dirarg)
核心逻辑:解析静态参数,支持切换动态参数、修饰符分支。
kotlin
private stateInDirArg(c: number): void {
// 情况1:= 或标签结束 → 参数结束
if (c === CharCodes.Eq || isEndOfTagSection(c)) {
this.cbs.ondirarg(this.sectionStart, this.index)
this.handleAttrNameEnd(c)
}
// 情况2:遇到 [ → 开启动态参数 v-dir:[arg]
else if (c === CharCodes.LeftSquare) {
this.state = State.InDirDynamicArg
}
// 情况3:遇到 . → 参数结束,进入修饰符
else if (c === CharCodes.Dot) {
this.cbs.ondirarg(this.sectionStart, this.index)
this.state = State.InDirModifier
this.sectionStart = this.index + 1
}
// 其他字符继续读取参数
}
stateInDynamicDirArg(状态:读取动态参数 xxx)
动态参数[] 必须成对闭合,未闭合直接抛出语法错误。
ts
private stateInDynamicDirArg(c: number): void {
// 情况1:匹配到 ] → 动态参数闭合,切回普通参数状态
if (c === CharCodes.RightSquare) {
this.state = State.InDirArg
}
// 情况2:还没遇到 ] 就碰到 = /标签结束 → 语法错误,缺少 ]
else if (c === CharCodes.Eq || isEndOfTagSection(c)) {
// index+1:把未闭合的动态参数范围完整上报
this.cbs.ondirarg(this.sectionStart, this.index + 1)
this.handleAttrNameEnd(c)
// 开发环境/非浏览器环境抛出错误:动态参数缺少结束方括号
if (__DEV__ || !__BROWSER__) {
this.cbs.onerr(
ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END,
this.index,
)
}
}
// 其它任意字符:继续留在 InDirDynamicArg 收集动态参数内容
}
stateInDirModifier(状态:读取修饰符 modifier,支持连续多个 .a.b.c)
ts
private stateInDirModifier(c: number): void {
// 情况1:= /标签结束 → 当前修饰符结束
if (c === CharCodes.Eq || isEndOfTagSection(c)) {
this.cbs.ondirmodifier(this.sectionStart, this.index)
this.handleAttrNameEnd(c)
}
// 情况2:遇到 . → 当前修饰符结束,开启下一个修饰符
else if (c === CharCodes.Dot) {
this.cbs.ondirmodifier(this.sectionStart, this.index)
this.sectionStart = this.index + 1
}
// 普通字符继续读取当前修饰符
}
6.3.3 指令三大核心回调源码+深度解读
ondirname (start, end) :指令名标准化转换回调
触发时机:状态机 InDirName 结束时调用,截取指令名字符串区间
核心能力:完成简写指令与标准指令的映射、v-pre 环境降级、非法指令校验。
ts
ondirname(start, end) {
// 1. 根据起止下标截取源码字符串
const raw = getSlice(start, end)
// ========== 指令简写语法转换 ==========
// :xxx → raw = ":" → name = bind
// @xxx → raw = "@" → name = on
// #xxx → raw = "#" → name = slot
// .xxx → raw = "." → name = bind
// v-on → raw = "v-on" → raw.slice(2) → "on"
const name =
raw === '.' || raw === ':'
? 'bind'
: raw === '@'
? 'on'
: raw === '#'
? 'slot'
: raw.slice(2)
// 2. 非v-pre环境,指令名称为空,抛出错误
// 例如 v- 后面没有名字:<div v->
if (!inVPre && name === '') {
emitError(ErrorCodes.X_MISSING_DIRECTIVE_NAME, start)
}
// ========== 分支1:v-pre 模式 或者 name为空 → 生成普通属性 ATTRIBUTE ==========
if (inVPre || name === '') {
currentProp = {
type: NodeTypes.ATTRIBUTE, // 普通属性,不再当做指令处理
name: raw, // 原始字符串原样保留(v- / : / @)
nameLoc: getLoc(start, end),
value: undefined,
loc: getLoc(start),
}
}
// ========== 分支2:正常指令,创建 DIRECTIVE 指令节点 ==========
else {
currentProp = {
type: NodeTypes.DIRECTIVE,
name, // 标准化指令名 bind/on/slot/model...
rawName: raw, // 原始源码字符串 v-bind / :
exp: undefined, // 指令表达式(等号后面的值,后续赋值)
arg: undefined, // 指令参数 :arg,后续 ondirarg 填充
modifiers:
// 特殊处理 .xxx 简写:v-bind.prop 简写 .prop,默认内置prop修饰符
raw === '.' ? [createSimpleExpression('prop')] : [],
loc: getLoc(start),
}
// ========== 特殊逻辑:v-pre 指令 ==========
if (name === 'pre') {
inVPre = tokenizer.inVPre = true // 开启v-pre全局标记
currentVPreBoundary = currentOpenTag
// ⭐重点:同一个标签内,v-pre前面已经解析出来的指令,全部强制转为普通属性
const props = currentOpenTag!.props
for (let i = 0; i < props.length; i++) {
if (props[i].type === NodeTypes.DIRECTIVE) {
props[i] = dirToAttr(props[i] as DirectiveNode)
}
}
}
}
},
核心规则总结:
- 统一语法映射:简写语法转为标准指令名,统一 AST 结构;
v-pre全局降级:所有指令失效,强制转为普通 HTML 属性;- 兜底校验:禁止空指令名,拦截非法语法。
ondirarg (start, end) 指令参数解析回调
触发时机:状态机解析完指令参数 :arg / :[dynamicArg]
核心能力:区分静态/动态参数、适配 v-pre 降级逻辑、生成参数表达式节点。
ts
ondirarg(start, end) {
// 起止下标相同:空参数,直接跳过
if (start === end) return
const arg = getSlice(start, end)
// ========== 分支1:处于v-pre 并且当前节点不是v-pre本身 ==========
// v-pre内不解析指令结构,把 :arg 直接拼接进属性name字符串
if (inVPre && !isVPre(currentProp!)) {
(currentProp as AttributeNode).name += arg
setLocEnd((currentProp as AttributeNode).nameLoc, end)
}
// ========== 分支2:正常指令环境,构建指令参数 arg ==========
else {
// 判断是否动态参数:[xxx] 形式
const isStatic = arg[0] !== `[`
;(currentProp as DirectiveNode).arg = createExp(
// 静态参数:直接使用文本;动态参数:去除首尾 []
isStatic ? arg : arg.slice(1, -1),
isStatic,
getLoc(start, end),
// 静态参数可常量优化;动态参数标记为非常量
isStatic ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT,
)
}
},
核心逻辑小结
- 空参数直接返回;
- v-pre 环境:不解析参数语义,直接拼接字符串到属性名称;
- 正常模式:区分静态参数
:foo和 动态参数:[foo],剥离方括号生成表达式节点挂载到directive.arg。
ondirmodifier (start, end) 修饰符解析回调
触发时机 :状态机解析 .modifier 修饰符,支持链式 .a.b.c
核心能力:适配 v-pre、v-slot 特殊场景,正常指令收集链式修饰符。
ts
ondirmodifier(start, end) {
const mod = getSlice(start, end)
// ========== 分支1:v-pre环境,非v-pre自身 ==========
// 修饰符 .modifier 直接拼接进属性名字符串,不做语义解析
if (inVPre && !isVPre(currentProp!)) {
(currentProp as AttributeNode).name += '.' + mod
setLocEnd((currentProp as AttributeNode).nameLoc, end)
}
// ========== 分支2:特殊特例 slot指令 ==========
// v-slot 不支持修饰符,遇到 .xxx 不新增modifier,而是拼接进 arg 参数内容
// 用于兼容边界场景模板
else if ((currentProp as DirectiveNode).name === 'slot') {
const arg = (currentProp as DirectiveNode).arg
if (arg) {
(arg as SimpleExpressionNode).content += '.' + mod
setLocEnd(arg.loc, end)
}
}
// ========== 分支3:普通指令,新增修饰符 ==========
else {
const exp = createSimpleExpression(mod, true, getLoc(start, end))
(currentProp as DirectiveNode).modifiers.push(exp)
}
},
核心逻辑小结
- v-pre:修饰符、参数、指令全部失效,纯文本拼接;
- v-slot:无修饰符语法,自动兼容兜底,合并至参数;
- 常规指令:修饰符存入数组,供后续代码生成、逻辑判断使用。