【源码阅读】Vue3 Tokenizer 词法分析器完整状态机流转深度梳理(3.4+ 新版架构)

Vue3 Tokenizer 词法分析器完整状态机流转深度梳理(3.4+ 新版架构)

一、前置总览:Vue 模板编译器完整链路

Vue 模板编译器的核心能力是将模板字符串 转换为可执行的 render 渲染函数,整体分为三大核心阶段,层层递进完成编译:

  1. Parser 解析阶段:模板字符串 → 词法分析 + 语法分析 → 模板 AST
  2. Transform 转换阶段:模板 AST → JavaScript AST
  3. Generate 生成阶段:JavaScript AST → render 函数字符串

完整执行链路:

graph LR 模板字符串 --> 词法分析 subgraph Parser->baseParse 词法分析[Tokenizer词法分析,有限状态机逐字符扫描] --> 语法分析[回调驱动构建节点] 语法分析 --> 模板AST end 模板AST --> Transform Transform --> JavaScriptAST JavaScriptAST --> Generate Generate --> 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.tsbaseCompile 中打印 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 整体执行流程:

flowchart LR 模板字符串输入 --> 重置上下文和合并配置 --> 初始化Tokenize--> 创建Root根节点-->tokenizer.parse状态机扫描构建AST -->优化空白节点 --> 返回完整AST

核心源码逻辑:

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 编译环境:手动解码 &nbsp;、< 等实体字符

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 大独立分组,覆盖模板所有语法场景,所有解析逻辑均围绕状态流转实现:

  1. 基础文本组 Text:默认初始状态,所有解析收尾默认回流至此
  2. 插值语法组 InterpolationFlow :解析 {{ expr }} 插值语法
  3. 标签解析组 TagRootFlow :处理所有以 &lt; 开头的标签场景
  4. 属性&指令组 AttrDirectiveFlow:解析原生属性 + Vue 全套指令语法(最复杂)
  5. 注释&声明组 DeclarationCommentFlow:解析注释、CDATA、DOCTYPE、处理指令
  6. RCDATA 原始文本组 RCDATAFlow:处理 script/style/title/textarea 特殊标签
  7. HTML 实体解码组 InEntity :解析 &xxx; 转义字符
  8. SFC 根标签专属组 InSFCRootTagName:适配 .vue 文件顶层标签解析规则

主状态流转图:

stateDiagram-v2 %% 顶层状态预先全部声明 [*] --> Text : 初始化 state Text state BeforeTagName state InterpolationOpen state InEntity state InterpolationFlow { state InterpolationOpen state Interpolation state InterpolationClose state InRCDATA InterpolationOpen --> InterpolationOpen : 匹配第二个左大括号 InterpolationOpen --> Interpolation : 完成双左大括号匹配 InterpolationOpen --> Text : 匹配失败,回退文本 InterpolationOpen --> InRCDATA : 处于RCDATA模式 Interpolation --> InterpolationClose : 字符为右大括号 InterpolationClose --> InterpolationClose : 匹配第二个右大括号 InterpolationClose --> Text : 完成双右大括号闭合,触发oninterpolation回调 InterpolationClose --> InRCDATA : RCDATA模式闭合后切回RCDATA InterpolationClose --> Interpolation : 闭合匹配失败回退插值内容 } state TagRootFlow { state BeforeTagName state BeforeDeclaration state InProcessingInstruction state BeforeSpecialS state BeforeSpecialT state InTagName state InSFCRootTagName state BeforeClosingTagName BeforeTagName --> BeforeDeclaration : 字符为感叹号(注释/CDATA/声明) BeforeTagName --> InProcessingInstruction : 字符为问号(处理指令) BeforeTagName --> BeforeSpecialS : 首字母s(script/style判断) BeforeTagName --> BeforeSpecialT : 首字母t(title/textarea判断) BeforeTagName --> InTagName : 普通标签字符 BeforeTagName --> InSFCRootTagName : SFC根节点下标签 BeforeTagName --> BeforeClosingTagName : 字符为斜杠(闭合标签) BeforeTagName --> Text : 非法标签起始,退回文本 } state OpenTagNameFlow { state InTagName state BeforeAttrName state InSFCRootTagName state SpecialStartSequence state BeforeSpecialS state BeforeSpecialT state InRCDATA InTagName --> BeforeAttrName : 空格、斜杠、大于号,标签名结束,触发onopentagname InSFCRootTagName --> BeforeAttrName : SFC根标签名结束 InSFCRootTagName --> SpecialStartSequence : SFC非template标签开启RCDATA BeforeSpecialS --> SpecialStartSequence : 匹配script/style前缀 BeforeSpecialT --> SpecialStartSequence : 匹配title/textarea前缀 SpecialStartSequence --> InTagName : 前缀匹配失败,普通标签 SpecialStartSequence --> InRCDATA : 前缀匹配成功,进入原始文本模式 } state AttrDirectiveFlow { state BeforeAttrName state InSelfClosingTag state Text state InAttrName state InDirName state InDirArg state AfterAttrName state InDirDynamicArg state InDirModifier state BeforeAttrValue state InAttrValueDq state InAttrValueSq state InAttrValueNq state InEntity BeforeAttrName --> InSelfClosingTag : 字符为斜杠(自闭合标签 ) BeforeAttrName --> Text : 字符为大于号 标签结束,触发onopentagend BeforeAttrName --> InAttrName : 普通属性名称开始 BeforeAttrName --> InDirName : 属性以v-开头v-bind/v-if指令 BeforeAttrName --> InDirArg : 属性以点、冒号、@、井号开头(指令参数) InAttrName --> AfterAttrName : 等号、空格、大于号 属性名结束 InDirName --> AfterAttrName : 等号、空格、大于号 指令名结束 InDirName --> InDirArg : 字符为冒号,指令参数分隔 InDirName --> InDirModifier : 字符为点,指令修饰符分隔 InDirArg --> AfterAttrName : 等号、空格、大于号 指令参数结束 InDirArg --> InDirDynamicArg : 字符为左方括号,动态参数v-bind动态键写法 InDirArg --> InDirModifier : 字符为点 修饰符 InDirDynamicArg --> InDirArg : 字符为右方括号 动态参数闭合 InDirModifier --> AfterAttrName : 等号、空格、大于号 修饰符结束 InDirModifier --> InDirModifier : 连续多点,多修饰符 AfterAttrName --> BeforeAttrValue : 字符为等号 进入属性值 AfterAttrName --> BeforeAttrName : 无属性值,直接下一个属性 BeforeAttrValue --> InAttrValueDq : 双引号包裹值 BeforeAttrValue --> InAttrValueSq : 单引号包裹值 BeforeAttrValue --> InAttrValueNq : 无引号属性值 InAttrValueDq --> BeforeAttrName : 遇到双引号闭合属性值 InAttrValueSq --> BeforeAttrName : 遇到单引号闭合属性值 InAttrValueNq --> BeforeAttrName : 空格、斜杠、大于号结束无引号值 InAttrValueDq --> InEntity : 属性内&实体编码 InAttrValueSq --> InEntity : 属性内&实体编码 InAttrValueNq --> InEntity : 属性内&实体编码 InSelfClosingTag --> Text : 字符为大于号 自闭合标签结束,触发onselfclosingtag InSelfClosingTag --> BeforeAttrName : 斜杠后还有属性,继续解析属性 } state CloseTagFlow { state BeforeClosingTagName state InClosingTagName state InSpecialComment state AfterClosingTagName BeforeClosingTagName --> InClosingTagName : 合法标签字符 BeforeClosingTagName --> InSpecialComment : 非法闭合标签,当做注释 BeforeClosingTagName --> Text : 直接遇到大于号,缺失标签名报错 InClosingTagName --> AfterClosingTagName : 空格、斜杠、大于号标签名结束,触发onclosetag AfterClosingTagName --> Text : 遇到大于号闭合标签结束 } state DeclarationCommentFlow { state BeforeDeclaration state CDATASequence state BeforeComment state InDeclaration state InCommentLike state InSpecialComment BeforeDeclaration --> CDATASequence : 字符为左方括号 匹配CDATA开头标记 BeforeDeclaration --> BeforeComment : 字符为减号 匹配注释开头 BeforeDeclaration --> InDeclaration : 普通DOCTYPE声明 CDATASequence --> InCommentLike : CDATA前缀匹配完成,等待结束标记 BeforeComment --> InCommentLike : 第二个减号完成注释起始,等待注释结束 InDeclaration --> Text : 遇到大于号结束声明 InCommentLike --> Text : 匹配注释或CDATA结束标记完成退出 InSpecialComment --> Text : 遇到大于号结束畸形注释 } state RCDATAFlow { state InRCDATA state InterpolationOpen state InEntity state CloseTagFlow InRCDATA --> InterpolationOpen : 匹配双左大括号插值(title/textarea支持插值) InRCDATA --> InEntity : title/textarea内实体解码 InRCDATA --> InRCDATA : 普通字符,累积文本 InRCDATA --> CloseTagFlow : 匹配script/style等闭合标签序列 } %% 顶层文本态核心分支 Text --> BeforeTagName : 字符为小于号 Text --> InterpolationOpen : 字符为双左大括号 且非inVPre状态 Text --> InEntity : 字符为& 且为非浏览器环境 Text --> Text : 普通字符,持续累积文本 %% 分支4:

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>

逐字符运行逻辑拆解

  1. 解析初始默认 State.Text,持续累积「普通文本 」字符,游标 index 持续自增;
  2. 当 index 匹配到第一个{ 字符,检测到非 v-pre 环境、命中插值起始符;
  3. 立即截断已累积的文本内容,触发 ontext 回调生成文本节点,切换状态为 InterpolationOpen,重置语法单元起始位 sectionStart;
  4. 插值解析完成后,状态自动回流 State.Text,继续解析「 尾部文本」,直至遇到标签闭合 < 符号。

5.2 插值语法完整闭环:Interpolation 三段式状态

插值采用三状态闭环 解析:InterpolationOpen(匹配{{)→ Interpolation(收集内容)→ InterpolationClose(匹配}}),完美兼容奇数 }、零散括号等边界场景,是模板动态内容解析的核心链路。

核心测试案例(含边界场景)

<span>姓名:{{ userName + '_' + age }} 测试}单括号{{ status }}</span>

1. stateInterpolationOpen:匹配插值开头 {{

通过 delimiterIndex 序列游标,精准匹配多字符插值分隔符,匹配失败不丢字符,自动回退重处理。

案例运行逻辑

  1. 文本状态解析至 {{ 首个 {,触发插值开启状态,delimiterIndex 从 0 开始自增匹配;
  2. 第二个 { 匹配完成,判定插值开头匹配成功;
  3. 截断前面「姓名:」文本并触发 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:匹配插值结尾 }}

核心容错:单个 } 判定为假终止,自动回退插值状态,避免解析报错。

边界场景运行逻辑

  1. 案例中第一个单独 }:仅匹配单个闭合符,delimiterIndex 未走完双符匹配逻辑,判定匹配失败;
  2. 自动回退 State.Interpolation 状态,将该 } 纳入插值内容,不终止解析;
  3. 直至匹配到完整 }},才判定插值结束,触发 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),
     })
 },

状态流转图:

stateDiagram-v2 direction LR [*] --> StateText StateText --> StateInterpolationOpen : 读到 '{', delimiterIndex=0 StateInterpolationOpen --> StateInterpolationOpen : 匹配 '{{' 第二位字符 StateInterpolationOpen --> StateInterpolation : ✅完整匹配 {{ StateInterpolationOpen --> StateText : ❌匹配失败 && !inRCDATA StateInterpolationOpen --> StateInRCDATA : ❌匹配失败 && inRCDATA StateInterpolation --> StateInterpolationClose : 读到 '}', delimiterIndex=0 StateInterpolationClose --> StateInterpolationClose : 匹配 '}}' 第二位字符 StateInterpolationClose --> StateText : ✅完整匹配 }} && !inRCDATA StateInterpolationClose --> StateInRCDATA : ✅完整匹配 }} && inRCDATA StateInterpolationClose --> StateInterpolation : ❌匹配失败(仅单个 })

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>

逐案例路由分发逻辑

  1. <!:匹配注释/CDATA/DOCTYPE,进入 BeforeDeclaration 状态,解析注释内容;
  2. <字母d:匹配普通自定义标签,进入 InTagName 状态,收集标签名;
  3. </:匹配闭合标签,进入 BeforeClosingTagName 状态,校验标签栈;
  4. <?:匹配 XML 处理指令,进入 InProcessingInstruction 状态;
  5. <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)
   }
 }

整体流程处理图:

flowchart LR A[&#34;进入 stateBeforeTagName(c)<br/>已读取字符 '<',c 是 '<' 的下一个字符&#34;] --> B{c 判断} B -->|c == !| C[&#34;state = BeforeDeclaration<br/>sectionStart = index + 1&#34;] B -->|c == ?| D[&#34;state = InProcessingInstruction<br/>sectionStart = index + 1&#34;] B -->|c == /| E[&#34;state = BeforeClosingTagName&#34;] B -->|c == isTagStartChar| F[&#34;sectionStart = index<br/>解析模式分支判断&#34;] B -->|其他字符| G[&#34;state = Text<br/>调用 stateText(c)&#34;] F --> H{模式判断} H -->|BASE模式| H1[&#34;state = InTagName&#34;] H -->|inSFCRoot| H2[&#34;state = InSFCRootTagName&#34;] H -->|!inXML HTML模式| H3{c 值} H -->|inXML XML模式| H4[&#34;state = InTagName&#34;] H3 -->|'t'| H31[&#34;state = BeforeSpecialT&#34;] H3 -->|'s'| H32[&#34;state = BeforeSpecialS&#34;] H3 -->|其余| H33[&#34;state = InTagName&#34;]

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" />

运行逻辑

  1. 命中< 进入标签分发,识别字母 b,进入标签名解析状态;
  2. 持续收集 button字符,遇到空格终止标签名收集;
  3. 触发 onopentagname 创建 button 元素节点,跳转至属性解析状态。

5.6 标签属性入口中枢:BeforeAttrName

标签名解析完成后进入此状态,分发四种场景:标签结束、自闭合标签、异常闭合、属性解析,是标签属性体系的核心路由。

核心逻辑:识别 > 结束标签、/ 自闭合、普通属性,兼容 IDE 增量编辑的异常 &lt;/ 场景。

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 状态,等待有效字符
 }

场景案例

  1. <div>:遇到 >,标签结束,生成空属性元素节点;
  2. <img src="a.png" />:遇到 /,判定自闭合标签,无需匹配闭合标签;
  3. <p class="text">:遇到普通字符,进入属性解析分支。

核心逻辑:识别 > 结束标签、/ 自闭合、普通属性,兼容 IDE 增量编辑的异常 </ 场景。

5.7 标签收尾:onopentagend / endOpenTag / onCloseTag

标签读到 > 后完成初始化,结合 <h1 :id="dynamicId">Vue Template</h1> 官方案例,完整运行流程:

  1. 挂载 h1 元素节点到根 AST 树,存入 props 指令属性;
  2. 判定非 Void 自闭合标签,将标签压入栈结构,用于嵌套校验;
  3. 无 v-pre、SVG 模式,保留默认解析规则;
  4. 文本解析完成后,匹配 </h1>闭合标签,弹出标签栈,回收上下文状态;
  5. 标记为普通 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 核心流程拆解

  1. SFC 根标签特殊位置初始化 innerLoc 用来记录标签内部内容起止位置,后续 onCloseTag 会修正结束偏移,用于 SFC 源码映射、代码高亮、IDE 提示。
  2. addNode(currentOpenTag!) 将组装好标签名、属性的 ElementNode 添加到父节点 children。
  3. pre 层级计数 inPre++ <pre> 内部需要保留原始空白,全局标记嵌套深度。
  4. Void 标签分支 HTML 规范 void 元素不需要闭合标签,直接调用 onCloseTag 完成收尾,不推入栈;解析器不会等待 </img>
  5. 普通元素分支 节点压入栈顶;如果是 SVG/MathML,打开 inXML 模式,XML 模式下标签大小写、自闭合规则更加严格。
  6. 清空临时变量 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 关键设计点逐条说明

  1. loc 位置修正

    • isImplied=true:HTML 容错隐式闭合(例如<p>aaa<div>自动关闭<p>),向前回溯找到起始 <
    • 正常闭合:向后查找 >,保证元素完整区间包含闭合标签。
  2. SFC innerLoc 完善 收集标签内部源码区间,对应 .vue 文件中 <style>内容</style> 的内容范围,用于热更新、源码映射。

  3. ElementTypes 类型标记(编译核心) 不在 v-pre 内部时区分四类元素:

    • ElementTypes.ELEMENT:普通 HTML 标签
    • ElementTypes.COMPONENT:自定义组件(首字母大写 / 带-
    • ElementTypes.TEMPLATE<template> 片段标签
    • ElementTypes.SLOT<slot> 插槽标签 后续代码生成阶段依靠 tagType 判断渲染逻辑。
  4. 空白压缩 condenseWhitespace Vue 经典特性:连续多个空白、换行压缩为单个空格;例外:inRCDATA=true(script/style)不压缩,保留原始源码。

  5. HTML 规范兼容:textarea/pre 首换行删除 HTML 标准规定:<textarea><pre> 标签紧跟的第一个换行符应当忽略,编译器在这里统一处理。

  6. v-pre 作用域回收 当闭合标签是 v-pre 所在元素,全局恢复 inVPre=false,重新开启插值解析。

  7. 命名空间切换 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
   }
 }

源码解读:

  1. 判断当前字符为 v 且下一位是 -,判定为完整指令,切换为「指令名解析状态」,记录解析起始位置;
  2. 匹配修饰符、绑定、事件、插槽简写首字符,提前触发指令名回调,直接进入「指令参数解析状态」;
  3. 其余所有字符,统一判定为普通 HTML 属性,进入「属性名解析状态」。

案例分发运行逻辑

  1. type:普通字母开头 → 进入普通属性解析分支;
  2. v-click:v- 前缀开头 → 进入完整指令名解析分支;
  3. :[dynamicAttr]:冒号开头 → 进入动态参数指令分支;
  4. @click:@ 开头 → 进入事件指令分支;
  5. .native:点号开头 → 进入指令修饰符分支。

6.2 普通属性完整流转链路

普通属性是 Vue 模板最基础的解析分支,完整状态链路为:

flowchart LR stateInAttrName解析属性名 --> AfterAttrName属性名结束 --> BeforeAttrValue等号后预处理 --> InAttrValueDq/Sq/Nq解析属性值-->onattribend属性收尾

该分支原生支持布尔无值属性、带引号属性、无引号属性、空白容错、重复属性校验全场景。

案例说明:

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 布尔属性为例)

  1. 持续收集 readonly 合法字符,无非法字符拦截;
  2. 属性名读取完成后,命中空白终止符,触发 onattribname 记录属性名;
  3. 无等号、无属性值,直接判定为布尔无值属性 ,后续自动赋值 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 无引号属性)

  1. 匹配 placeholder= 后,跳过末尾空白;
  2. 首字符为普通文本,进入 InAttrValueNq 无引号模式
  3. 持续收集字符,直至命中下一个空白终止属性值。

三种属性值解析状态源码+案例

双引号属性值 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:非浏览器环境遇到 &,启动实体解析 &nbsp;
     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、修饰符=stop
  • v-model:[field].trim.number:指令名=model、动态参数=field、双修饰符=trim/number

指令专属状态集合:

  1. InDirName:读取指令名
  2. InDirArg:读取指令参数
  3. InDirDynamicArg:读取动态方括号参数 [xxx]
  4. 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
     }
     // 普通字符继续读取当前修饰符
 }
flowchart LR A[InDirName<br/>解析指令名] B[InDirArg<br/>解析普通指令参数] C[InDirDynamicArg<br/>解析动态参数] D[InDirModifier<br/>解析修饰符] E[AttrEnd<br/>属性名称解析完成] A -- &#34;= / 标签结束符&#34; --> E A -- &#34;:&#34; --> B A -- &#34;.&#34; --> D B -- &#34;= / 标签结束符&#34; --> E B -- &#34;[&#34; --> C B -- &#34;.&#34; --> D C -- &#34;]&#34; --> B C -- &#34;= / 标签结束符 ⚠缺失]&#34; --> E D -- &#34;= / 标签结束符&#34; --> E D -- &#34;.&#34; --> D
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,
       )
     }
 },

核心逻辑小结

  1. 空参数直接返回;
  2. v-pre 环境:不解析参数语义,直接拼接字符串到属性名称;
  3. 正常模式:区分静态参数 :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)
     }
 },

核心逻辑小结

  1. v-pre:修饰符、参数、指令全部失效,纯文本拼接;
  2. v-slot:无修饰符语法,自动兼容兜底,合并至参数;
  3. 常规指令:修饰符存入数组,供后续代码生成、逻辑判断使用。
相关推荐
PBitW1 小时前
PM 丢来 3 个 Excel、12 个功能、4 种情形?用 TRAE Work 30 分钟整理成前端开发文档
前端·trae
风骏时光牛马3 小时前
AIAgent高可用架构:弹性容错与故障自愈的落地实践
前端
IT_陈寒3 小时前
Vue的响应式什么时候会失灵?这个坑我踩了
前端·人工智能·后端
烟锁池塘柳04 小时前
解决 Firefox 浏览器网页文本选中高亮不明显问题(含仅修改特定网站高亮显示的方案)
前端·firefox
Patrick_Wilson4 小时前
Web 认证方案技术指南
前端·后端·面试
YuJie4 小时前
JSBridge 基础知识
前端·javascript
BigTopOne4 小时前
Ubuntu 虚拟机编译 WebRTC Android AAR(M140 / branch-heads/7339)
前端
默_笙4 小时前
🍕 后端接口还没写好,前端已经跑起来了?Mock 数据 + axios 接口层,前后端再也不互相"等"
前端·javascript
书源4 小时前
AI 时代写给前端同行:什么在贬值,什么在涨价
前端·程序员·ai编程