普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月4日技术

React 中 useCallback 的基本使用和原理解析

2025年11月4日 23:15

React 中 useCallback 的基本使用方法

useCallback 是 React 的一个核心 Hook,用于缓存函数定义,避免组件重新渲染时重复创建函数实例。以下是其基本使用方法:

1. 基本语法

const memoizedCallback = useCallback(
  () => {
    // 函数逻辑 (例如更新状态、调用API等)
    doSomething(a, b);
  },
  [a, b] // 依赖项数组
);
  • 第一个参数:需要缓存的函数。
  • 第二个参数:依赖项数组(Dependency Array),当数组中的变量变化时,函数会重新创建。

2. 核心作用

  • 避免不必要的函数重建:默认情况下,组件每次渲染都会创建新的函数实例,使用 useCallback 后可复用函数。
  • 优化子组件渲染:当缓存的函数作为 props 传递给子组件(配合 React.memo)时,可避免子组件不必要的重渲染。

3. 使用示例

import React, { useState, useCallback } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  // 缓存函数:依赖项为空数组,函数只创建一次
  const increment = useCallback(() => {
    setCount(prev => prev + 1); // 使用函数式更新避免闭包问题
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
    </div>
  );
}
  • 依赖项 [] 表示函数仅在组件初次渲染时创建。
  • 使用 setCount(prev => prev + 1) 替代 setCount(count + 1) 可避免闭包陷阱(函数捕获过时状态)。

4. 适用场景

useCallback,本质上是用于缓存函数。

如果函数,是以props的方式,传递给子组件,为了每次避免子组件的渲染,建议使用useCallback进行包裹。

但是每一次,使用useCallback,我们考虑的首要问题是,这样真的优化了组件的性能吗?其实大多数场景,如果不是类似列表渲染的场景,这样不一定会优化了性能。

也就是,函数作为props传递给性能敏感的子组件的场景,才是使用useCallback的时候。

useCallback 的原理解析

  • useCallback 的主要目的是在依赖项不变的情况下,返回同一个函数引用,避免函数重复创建,从而优化性能。
  • useCallback它会在首次渲染时(或依赖项变化时)创建一个新的函数,并将其缓存起来。在后续渲染中,如果依赖项没有变化,则返回缓存的函数;否则,就重新创建函数并更新缓存。
  • 简易的伪代码,可能如下所示
let lastDeps; // 上一次的依赖项
let lastCallback; // 上一次缓存的函数

function useCallback(callback, deps) {
  if (lastDeps === undefined) {
    // 第一次调用
    lastDeps = deps;
    lastCallback = callback;
    return callback;
  }

  // 检查依赖项是否变化
  const hasChanged = deps.some((dep, index) => dep !== lastDeps[index]);
  if (hasChanged) {
    lastDeps = deps;
    lastCallback = callback;
  }
  return lastCallback;
}

每次掉用useCallback,返回的函数,取决于依赖项有没有发生变化。

React内部是咋样的呢?

1、Fiber 节点存储机制

React 在 Fiber 节点(组件实例对应的数据结构)中维护一个 memorizedState 链表,专门存储 Hooks 状态。

function updateCallback(callback, deps) {
  const hook = updateWorkInProgressHook(); // 获取当前 Hook 节点
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;     // 读取缓存的上次状态
  
  // 依赖项对比:使用浅比较(shallow equal)
  if (prevState !== null && areHookInputsEqual(nextDeps, prevState[1])) {
    return prevState[0]; // 返回缓存的函数
  }
  
  //  依赖变化:缓存新函数
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

2、依赖项对比算法

源码中的 areHookInputsEqual 对依赖数组进行浅比较(类似 Object.is):

function areHookInputsEqual(nextDeps, prevDeps) {
  if (prevDeps === null) return false;
  for (let i = 0; i < prevDeps.length; i++) {
    if (!Object.is(nextDeps[i], prevDeps[i])) {
      return false;
    }
  }
  return true;
}

这种优化避免了深度比较的性能损耗

Safari 中文输入法的诡异 Bug:为什么输入 @ 会变成 @@? ## 开头 做 @ 提及功能的时候,测试同学用 Safari 测出了个奇怪的问题

2025年11月4日 23:00

Safari 中文输入法的诡异 Bug:为什么输入 @ 会变成 @@?

开头

做 @ 提及功能的时候,测试同学用 Safari 测出了个奇怪的问题——输入框里按一下 @ 键,结果出现了两个 @@

更诡异的是:

  • Chrome 上正常,只有一个 @
  • Safari 用英文输入法也正常
  • 只有 Safari + 中文输入法会重复

第一反应是"我代码写错了",但看了半天逻辑没毛病啊。难道是浏览器的 Bug?

深入研究才发现,这是 Safari 在处理中文输入法时的特殊行为,涉及到 compositionend 事件和 beforeinput 事件的微妙差异。更有意思的是,这个问题还让我重新理解了一个问题:为什么中文输入需要"合成",而英文直接映射就行?

问题复现

测试场景

代码逻辑很简单:在 keydown 阶段监听 @ 键,插入 @ 字符并弹出用户列表。

// 简化的问题代码
const handleKeyDown = (e: React.KeyboardEvent) => {
  if (e.key === '@') {
    e.preventDefault();
    
    // 手动插入 @ 字符
    insertText('@');
    
    // 显示用户列表弹窗
    showMentionPopup();
  }
};

诡异的现象

浏览器 输入法 输入 @ 后的结果
Chrome 中文 @
Chrome 英文 @
Safari 英文 @
Safari 中文 @@

问题稳定复现,但只在一个特定组合下出现:Safari + 中文输入法

深入原因

事件触发顺序对比

先来看看正常情况下的事件流:

Chrome(正常):

1. keydown (key='@')
2. preventDefault() 阻止默认行为
3. ✅ 后续 beforeinput、input 事件都不会触发

Safari + 中文输入法(异常):

1. keydown (key='@')
2. preventDefault() 在 keydown 阶段生效
3. compositionstart (输入法激活)
4. compositionend (输入法合成完成)
5. ❌ beforeinput 事件竟然还是触发了!(data='@')
6. input 事件触发,字符被插入

关键差异在第 5 步:Safari 在 compositionend 后重新发起了 beforeinput 事件,完全无视之前在 keydown 阶段调用的 preventDefault()

为什么会有两个 @?

sequenceDiagram
    participant User as 用户
    participant IME as 中文输入法
    participant Safari as Safari 浏览器
    participant Handler as 事件处理器

    User->>IME: 按下 @ 键
    IME->>Safari: keydown event
    Safari->>Handler: 触发 handleKeyDown
    
    rect rgb(255, 200, 200)
        Note over Handler: ❌ 第一次插入
        Handler->>Handler: preventDefault()
        Handler->>Handler: insertText('@')
        Note right of Handler: segments = ['@']
    end
    
    Note over IME,Safari: Safari 的特殊行为
    IME->>Safari: compositionstart
    Safari->>Safari: 输入法激活
    IME->>Safari: compositionend
    
    rect rgb(255, 200, 200)
        Note over Safari: ❌ Safari 忽略了之前的 preventDefault
        Safari->>Handler: beforeinput (data='@')
        Handler->>Handler: insertText('@')
        Note right of Handler: segments = ['@', '@']
    end

双重插入的本质

  1. keydown 阶段:我们手动插入了第一个 @
  2. compositionend 后:Safari 认为"输入法合成完成了,该插入字符了",重新触发 beforeinput,又插入了第二个 @

为什么只有中文输入法有问题?

这就要从输入法的原理说起了。

英文输入(直接映射)

按键 'A' → 字符 'A'(一对一)
按键 '@' → 字符 '@'(一对一)
  • 英文字母只有 26 个 + 数字 10 个 + 符号几十个
  • 标准键盘有 104 个按键
  • 键盘够用,所以可以直接映射
  • 不需要输入法参与

中文输入(需要合成)

按键 'n' 'i' 'h' 'a' 'o' → 需要输入法处理 → '你好'
  • 常用汉字 3500+ 个
  • 标准键盘只有 104 个按键
  • 键盘远远不够
  • 必须通过输入法合成:多个按键 → 一个汉字

那为什么 @ 也要合成?

理论上 @ 这种单字符完全可以直接映射,不需要走输入法。但实际中文输入法的实现是这样的:

// 中文输入法的实现逻辑(简化)
class ChineseIME {
  isActive = true;  // 输入法始终激活
  
  onKeyPress(key) {
    // ❌ 为了统一处理,所有按键都启动合成
    this.startComposition();
    
    if (this.needsCandidates(key)) {
      // 字母:显示拼音候选
      this.showCandidates();
    } else {
      // 标点:立即结束合成
      this.endComposition(key);
    }
  }
}

为什么要这样设计?

  1. 状态机简化 - 统一处理比特殊判断简单
  2. 标点歧义 - 某些标点有全角/半角之分(, vs
  3. 词库扩展 - 现代输入法把符号也加入候选(输入 haha 可能出现 😄)
  4. 历史包袱 - 早期输入法这样设计,沿用至今

所以即使是 @,中文输入法也会走完整的 compositionstartcompositionend 流程。

Safari 为什么特殊?

Chrome 的逻辑

// Chrome 记住了 preventDefault 标记
keydown.preventDefault()
  → 标记该按键"已阻止"
  → compositionend 后检查标记
  → 发现"已阻止"
  → 不触发 beforeinput ✅

Safari 的逻辑

// Safari 把 composition 当作独立流程
keydown.preventDefault()
  → 只影响 keydown 自己
  → compositionend 后
  → 重新评估"是否该插入字符"
  → 触发 beforeinput ❌

本质是 Safari 将 composition 流程视为独立的输入事件链,不继承 keydown 阶段的 preventDefault() 状态。

修复方案

核心思路

问题根源:在 keydown 阶段手动插入字符,与 Safari 后续的 beforeinput 冲突。

解决方法:不在 keydown 插入字符,统一在 beforeinput 阶段处理。

修复前的代码

const handleKeyDown = (e: React.KeyboardEvent) => {
  if (e.key === '@' && supportMention) {
    e.preventDefault();
    
    // ❌ 问题:在 keydown 阶段手动插入
    handleSegmentChange({
      changeType: ESegmentChangeType.Add,
      changeInfo: {
        start, end,
        contentType: InputSegmentType.Text,
        content: '@',  // ← 第一次插入
      },
    });
    
    // 显示弹窗
    setShowMentionList(true);
    inputRef.current?.blur();  // ❌ 失焦,无法继续输入
  }
};

// ❌ Safari 会在 compositionend 后再次触发
const handleBeforeInput = (e: React.FormEvent) => {
  const inputEvent = e.nativeEvent as InputEvent;
  
  if (inputEvent.data) {
    handleSegmentChange({
      changeType: ESegmentChangeType.Add,
      changeInfo: {
        content: inputEvent.data,  // ← 第二次插入 '@'
      },
    });
  }
};

修复后的代码

const mentionTriggerIndexRef = useRef<number | null>(null);

const handleKeyDown = (e: React.KeyboardEvent) => {
  if (e.key === '@' && supportMention) {
    e.preventDefault();
    
    // ✅ 改进:只记录位置,不插入字符
    const { start } = getCursorRange(inputRef.current);
    mentionTriggerIndexRef.current = start;
    
    // 显示弹窗
    setShowMentionList(true);
    setMentionKeyword('');
    
    // 计算弹窗位置
    const { left, bottom } = getCursorPositionPx(inputRef.current) || {};
    setMentionPosition({ left: 12 + left, bottom });
    
    // ✅ 保持焦点,允许继续输入
    inputRef.current?.focus();
  }
};

// beforeinput 会自然触发,插入 @(只插入一次)
const handleBeforeInput = (e: React.FormEvent) => {
  const inputEvent = e.nativeEvent as InputEvent;
  
  if (inputEvent.data) {
    handleSegmentChange({
      changeType: ESegmentChangeType.Add,
      changeInfo: {
        content: inputEvent.data,  // ✅ 只会插入一次
      },
    });
  }
};

// 在 handleSegmentChange 后更新 mention 追踪
const updateMentionTracking = useCallback(
  (nextRawText: string, cursorPosition: number) => {
    if (!showMentionList) return;
    
    const triggerIndex = mentionTriggerIndexRef.current;
    if (triggerIndex === null) return;
    
    // 提取 @ 后的搜索词
    const keyword = nextRawText.slice(triggerIndex + 1, cursorPosition);
    
    // 包含空格则关闭弹窗
    if (/\s/.test(keyword)) {
      closeMentionList();
      return;
    }
    
    // 更新搜索关键词
    setMentionKeyword(keyword);
  },
  [showMentionList, closeMentionList]
);

关键改动点

改动项 修复前 修复后
字符插入 keydown 手动插入 beforeinput 自然插入
位置记录 isMentioningSaveRangeRef(需要 +1/-1) mentionTriggerIndexRef(直接记录)
焦点管理 blur() 失焦 focus() 保持焦点
状态追踪 在 keydown 阶段设置 在 handleSegmentChange 后更新

修复后的事件流

image.png

效果

  • Chrome:beforeinputpreventDefault() 阻止,用户继续输入其他字符
  • Safari:beforeinput 触发,插入 @(只插入一次),然后更新追踪状态

技术要点总结

1. 不要在 keydown 中插入普通字符

// ❌ 错误做法
handleKeyDown = (e) => {
  if (e.key === 'a') {
    e.preventDefault();
    insertText('a');  // ← 可能导致重复插入
  }
};

// ✅ 正确做法
handleKeyDown = (e) => {
  if (e.key === 'a') {
    // 只设置标记,不插入字符
    setShouldDoSomething(true);
  }
};

handleBeforeInput = (e) => {
  // 统一在这里处理字符插入
  insertText(e.data);
};

原则

  • keydown:处理快捷键、特殊按键(Enter、Backspace、Escape)
  • beforeinput:统一处理字符插入
  • compositionstart/end:管理输入法状态

2. preventDefault() 的作用范围

e.preventDefault();  // 只阻止当前事件的默认行为

// 不会阻止的(在 Safari 中文模式下):
// - compositionstart/end
// - beforeinput(composition 后重新发起)
// - input

如果要阻止字符插入,应该在 beforeinput 阶段调用 preventDefault(),而不是 keydown

3. 使用 ref 记录辅助数据

// ✅ 使用 ref(不触发重渲染)
const mentionTriggerIndexRef = useRef<number | null>(null);
mentionTriggerIndexRef.current = start;

// ❌ 不要用 state(会触发重渲染,性能差)
const [mentionTriggerIndex, setMentionTriggerIndex] = useState<number | null>(null);

选择原则

  • useState:需要触发 UI 更新的数据(如搜索关键词、选中索引)
  • useRef:辅助计算的数据(如触发位置、缓存值)

4. 浏览器兼容性测试的重要性

这个 Bug 提醒我们:

  • ✅ 不要假设所有浏览器行为一致
  • ✅ 核心功能必须在 Safari、Chrome、Firefox 中测试
  • ✅ 特别注意 Safari + 中文输入法 这种组合

浏览器事件机制对比

标准输入事件流

flowchart TD
    A[用户按键] --> B[keydown]
    B --> C{是否 composing?}
    
    C -->|否 英文输入| D[beforeinput]
    D --> E[input]
    E --> F[keyup]
    
    C -->|是 中文输入| G[compositionstart]
    G --> H[compositionupdate]
    H --> I[compositionend]
    I --> J[beforeinput]
    J --> K[input]
    K --> L[keyup]
    
    style D fill:#c8e6c9
    style J fill:#fff9c4

preventDefault() 在不同浏览器的表现

浏览器 keydown preventDefault() 是否阻止后续 beforeinput?
Chrome ✅ 阻止所有后续事件 ✅ 是
Firefox ✅ 阻止所有后续事件 ✅ 是
Safari(英文) ✅ 阻止所有后续事件 ✅ 是
Safari(中文) ⚠️ 仅阻止 keydown 阶段 ❌ 否(composition 后仍触发)

为什么中文需要合成?一个有趣的对比

字符数量的差异

英文字母: 26 个
+ 大写: 26 个
+ 数字: 10 个
+ 符号: ~20 个
= 总共约 80 个字符

标准键盘: ~104 个按键

✅ 键盘够用 → 可以直接映射
常用汉字: 3,500 个
GB2312: 6,763 个
Unicode: 20,000+ 个

标准键盘: ~104 个按键

❌ 键盘远远不够 → 必须用合成方案

不同语言的输入方式

英文(直接映射)

按键 A → 字符 A
按键 @ → 字符 @

中文拼音(音码)

输入: z h o n g g u o (8 个按键)
显示: 拼音候选 → 中国、钟国、忠国...
选择: 按空格或数字
输出: 中国 (2 个汉字)

日文(两层合成)

输入: a r i g a t o u
第一层: ありがとう (平假名)
转换键: 按空格
第二层: 有難う (汉字+假名)
输出: 有難う

韩文(字母拼合)

输入: ㄱ + ㅏ + ㅁ (3 个字母)
拼合: ㄱ → 가 → 감
输出: 감 (1 个音节块)

为什么 @ 也要走合成流程?

这是工程妥协,而非必然设计:

  1. 状态机简化 - 统一处理所有按键,代码更简单
  2. 标点歧义 - 某些标点有全角/半角之分(, vs
  3. 词库扩展 - 现代输入法支持 emoji 和符号候选
  4. 历史兼容 - 早期设计被沿用至今

如果输入法对 @ 特殊处理,会导致:

用户输入: n i h a o @

如果 @ 直接插入:
  nihao → (合成中)
  @ → (直接插入) ❌ 破坏合成状态!
  用户无法继续输入

统一走合成:
  nihao → (合成中)
  用户确认 → (插入"你好",结束)
  @ → (新合成,立即结束) ✅ 状态一致

总结

研究完这个 Bug,我的理解是:

问题本质

  • Safari 将 composition 流程视为独立事件链
  • compositionend 后重新发起 beforeinput
  • 忽略了之前在 keydown 阶段的 preventDefault()

修复原则

  • ❌ 不在 keydown 中插入普通字符
  • ✅ 在 beforeinput 统一处理字符插入
  • keydown 仅用于特殊按键(Enter、Backspace、Escape)
  • ✅ 使用 ref 记录辅助数据,避免不必要的重渲染

深层收获

  • 理解了为什么中文需要"合成":键盘按键数 << 汉字数量
  • 理解了为什么 @ 也走合成流程:输入法的统一状态管理
  • 意识到浏览器兼容性测试的重要性:Safari + 中文输入法 是个特殊组合

实用建议

  1. 核心功能必须在 Safari 上测试,特别是输入相关的
  2. 不要假设 preventDefault() 能阻止所有后续事件
  3. 事件处理职责分离:keydown 处理特殊键,beforeinput 处理字符插入
  4. 使用 ref 存储不需要触发渲染的辅助数据

下次遇到类似的输入问题,你会知道:

  • 先看事件触发顺序(打印日志)
  • 检查是否在 keydown 阶段插入了字符
  • 在 Safari + 中文输入法下测试
  • composition 流程可能带来意外的事件触发

如果你的项目也有 @ 提及、# 话题这类功能,建议现在就去 Safari 上测一测。数据显示,Safari 在中国的市场份额约 20%(主要是 iOS 用户),别让这 20% 的用户遇到诡异的重复输入问题。

参考资料

W3C 标准文档

  1. UI Events Specification - Composition Events - composition 事件规范
  2. Input Events Level 2 - beforeinput event - beforeinput 事件规范

MDN 文档

  1. CompositionEvent - composition 事件详解
  2. InputEvent - input 事件详解
  3. Event.preventDefault() - preventDefault 的作用范围

浏览器差异

  1. WebKit Bugzilla - Safari 已知问题
  2. Chromium Issue Tracker - Chrome 事件处理实现

相关文章

  1. IME (Input Method Editor) 原理 - 输入法编辑器工作原理

今日苹果 App Store 前端源码泄露,赶紧 fork 一份看看

作者 冴羽
2025年11月4日 21:52

新闻

今日苹果 App Store 前端源码泄露,仓库地址:github.com/rxliuli/app…

仅仅过去了十几个小时,就已经 fork 上千份,star 过千了 😂

泄露原因

所以它是怎么泄露的呢?

因为苹果忘记在 App Store 网站的生产环境中禁用 sourcemap 了 😂

然后就被存档上传了一份,哈哈哈哈!

这份代码仓库里有:

  • 完整的 Svelte/TypeScript 源代码
  • 状态管理逻辑
  • UI组件
  • API集成代码
  • 路由配置

稍后让我替大家细看一下这份代码~

🔍 深度解析:Vue 编译器中的 validateBrowserExpression 表达式校验机制

作者 excel
2025年11月4日 18:46

一、背景与概念说明

在 Vue 3 的编译阶段中,模板(template)需要被解析成 JavaScript 表达式。例如:

<div>{{ user.name }}</div>

会被编译为:

_createElementVNode("div", null, _toDisplayString(user.name))

然而,模板中的表达式必须是合法的 JavaScript 语法,同时不能包含某些保留关键字(如 for, while, class 等)。
因此,Vue 编译器需要一个安全机制去检测表达式是否合法——这正是 validateBrowserExpression 函数的职责。


二、源码概览

import type { SimpleExpressionNode } from './ast'
import type { TransformContext } from './transform'
import { ErrorCodes, createCompilerError } from './errors'

// 1️⃣ 定义不允许出现在表达式中的关键字
const prohibitedKeywordRE = new RegExp(
  '\b' +
    (
      'arguments,await,break,case,catch,class,const,continue,debugger,default,' +
      'delete,do,else,export,extends,finally,for,function,if,import,let,new,' +
      'return,super,switch,throw,try,var,void,while,with,yield'
    )
      .split(',')
      .join('\b|\b') +
    '\b',
)

// 2️⃣ 定义用于剔除字符串字面量的正则(防止误匹配)
const stripStringRE =
  /'(?:[^'\]|\.)*'|"(?:[^"\]|\.)*"|`(?:[^`\]|\.)*${|}(?:[^`\]|\.)*`|`(?:[^`\]|\.)*`/g

/**
 * 3️⃣ 表达式验证函数
 * 主要在浏览器端运行时编译器中调用
 */
export function validateBrowserExpression(
  node: SimpleExpressionNode,
  context: TransformContext,
  asParams = false,
  asRawStatements = false,
): void {
  const exp = node.content

  // ① 空表达式情况(例如 v-if="")由上层指令处理
  if (!exp.trim()) {
    return
  }

  try {
    // ② 构造一个 Function 来检测表达式语法是否合法
    new Function(
      asRawStatements
        ? ` ${exp} `
        : `return ${asParams ? `(${exp}) => {}` : `(${exp})`}`,
    )
  } catch (e: any) {
    // ③ 捕获语法错误并进一步检查是否包含关键字
    let message = e.message
    const keywordMatch = exp
      .replace(stripStringRE, '')
      .match(prohibitedKeywordRE)
    if (keywordMatch) {
      message = `avoid using JavaScript keyword as property name: "${keywordMatch[0]}"`
    }
    // ④ 通过上下文的 onError 报告错误
    context.onError(
      createCompilerError(
        ErrorCodes.X_INVALID_EXPRESSION,
        node.loc,
        undefined,
        message,
      ),
    )
  }
}

三、原理解析

1️⃣ 关键逻辑:用 new Function() 检测表达式是否合法

new Function(`return (${exp})`)

这一技巧利用了 JavaScript 引擎本身的语法检查能力

  • 如果表达式语法错误,会直接抛出 SyntaxError
  • 如果语法合法,则不会报错,说明可安全用于运行时求值。

例如:

new Function('return (user.name)')  // ✅ 通过
new Function('return (if)')         // ❌ SyntaxError: Unexpected token 'if'

2️⃣ 防止关键字误用

Vue 不希望用户写出类似:

<div>{{ class }}</div>

虽然这是合法的 JS 标识符(在模板上下文中可能被误解析),但会与 JS 关键字冲突。
因此,使用正则 prohibitedKeywordRE 检测关键字出现。

注意这里的关键点:

  • 先使用 stripStringRE 去掉字符串字面量,防止 "return" 这种字符串触发误报。
  • 然后再匹配关键字。

3️⃣ 错误汇报机制

通过 context.onError 统一抛出编译阶段错误:

context.onError(
  createCompilerError(
    ErrorCodes.X_INVALID_EXPRESSION,
    node.loc,
    undefined,
    message,
  )
)

这会被编译器统一捕获并转化为编译日志或提示信息。


四、对比分析

特性 validateBrowserExpression Vue 服务器端编译器 (SSR) Babel 等工具
检查方式 运行时 new Function() 静态 AST 解析 语法树静态分析
运行环境 浏览器 Node.js 通用
目的 快速语法检测 + 安全关键字过滤 静态优化 + 安全执行 完整语言解析
性能 快速、轻量 相对较重 较慢但最精确

五、实践示例

✅ 合法表达式

<div>{{ user.age + 1 }}</div>

验证过程:

  1. exp = "user.age + 1"
  2. new Function("return (user.age + 1)") ✅ 无异常
  3. 校验通过。

❌ 非法表达式(语法错误)

<div>{{ if user.age }}</div>

验证过程:

  1. 抛出 SyntaxError: Unexpected identifier
  2. 捕获错误 → 报告 X_INVALID_EXPRESSION

⚠️ 关键字误用

<div>{{ class }}</div>

验证过程:

  1. 语法层面 new Function 不报错(因为 class 是保留字)

  2. 但关键字匹配命中 → 提示:

    avoid using JavaScript keyword as property name: "class"
    

六、拓展思考

  1. 安全性
    new Function() 在编译器中使用是安全的,因为它只执行语法检查,不执行结果。但若在运行时执行用户输入,则会有安全风险。
  2. 替代方案
    在更严格的环境中,可以使用 AST 解析器(如 @babel/parser)进行安全检测。
  3. 兼容性
    某些浏览器中对 new Function() 的语法报错信息不同,因此 Vue 使用自定义错误代码 (ErrorCodes.X_INVALID_EXPRESSION) 统一处理。

七、潜在问题与优化方向

问题点 说明 可能优化
错误定位不精确 只能指出哪一条表达式出错,不能指出字符位置 可结合 AST 报错精确行列
关键字正则维护复杂 新的 JS 关键字需手动更新 可自动生成关键字列表
性能瓶颈 大量表达式时多次构造 Function 对象 可在编译时缓存校验结果

八、总结

validateBrowserExpression 是 Vue 编译器的核心安全防线之一,它通过:

  • new Function() 检查表达式语法;
  • 正则匹配禁止关键字;
  • 报告编译错误;

实现了轻量、快速且安全的模板表达式验证。

这一实现方案在运行时编译环境中兼顾了性能与安全性,为 Vue 模板的动态编译提供了强有力的保障。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

深入解析:Vue 编译器核心工具函数源码(compiler-core/utils.ts)

作者 excel
2025年11月4日 18:45

一、概念与背景

在 Vue 3 的模板编译流程中,compiler-core 是整个编译链的心脏模块之一。
它负责将模板语法 (<template>...</template>) 转化为虚拟 DOM 渲染函数(render)。
而其中的 utils.ts 文件,提供了一系列“编译辅助工具函数”,用于:

  • 表达式判断与解析(如 isMemberExpression, isFnExpression
  • 节点属性分析与注入(如 findProp, injectProp
  • 位置信息与错误处理(如 advancePositionWithMutation, assert
  • 作用域与上下文检测(如 hasScopeRef

这些工具是编译器在“语义判断”与“代码生成”阶段的中间层逻辑支撑。


二、核心原理分解

1. 静态表达式判断:isStaticExp

export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
  p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic

原理:
判断一个 AST 节点是否为“简单静态表达式”(即内容在编译期可确定的常量)。

注释说明:

  • NodeTypes.SIMPLE_EXPRESSION → 表示 {{ message }}v-bind:foo="bar" 中的 bar
  • isStatic → 编译器在解析阶段标记的属性,用于区分“动态 vs 静态”节点。

2. 核心组件识别:isCoreComponent

export function isCoreComponent(tag: string): symbol | void {
  switch (tag) {
    case 'Teleport':
    case 'teleport':
      return TELEPORT
    case 'Suspense':
    case 'suspense':
      return SUSPENSE
    case 'KeepAlive':
    case 'keep-alive':
      return KEEP_ALIVE
    case 'BaseTransition':
    case 'base-transition':
      return BASE_TRANSITION
  }
}

原理:
将内置组件(Teleport、Suspense、KeepAlive、BaseTransition)映射为编译时符号。
这些符号用于生成渲染函数时调用特定的 runtime helper。


3. 表达式词法分析:isMemberExpressionBrowser

export const isMemberExpressionBrowser = (exp: ExpressionNode): boolean => {
  const path = getExpSource(exp)
    .trim()
    .replace(whitespaceRE, s => s.trim())

  // 状态机初始化
  let state = MemberExpLexState.inMemberExp
  let stateStack: MemberExpLexState[] = []
  let currentOpenBracketCount = 0
  let currentOpenParensCount = 0
  let currentStringType: "'" | '"' | '`' | null = null

  for (let i = 0; i < path.length; i++) {
    const char = path.charAt(i)
    switch (state) {
      case MemberExpLexState.inMemberExp:
        if (char === '[') {
          stateStack.push(state)
          state = MemberExpLexState.inBrackets
          currentOpenBracketCount++
        } else if (char === '(') {
          stateStack.push(state)
          state = MemberExpLexState.inParens
          currentOpenParensCount++
        } else if (
          !(i === 0 ? validFirstIdentCharRE : validIdentCharRE).test(char)
        ) {
          return false
        }
        break
      case MemberExpLexState.inBrackets:
        if (char === `'` || char === `"` || char === '`') {
          stateStack.push(state)
          state = MemberExpLexState.inString
          currentStringType = char
        } else if (char === `[`) {
          currentOpenBracketCount++
        } else if (char === `]`) {
          if (!--currentOpenBracketCount) {
            state = stateStack.pop()!
          }
        }
        break
      ...
    }
  }
  return !currentOpenBracketCount && !currentOpenParensCount
}

原理:
这段代码实现了一个简易 状态机词法分析器,判断字符串是否是合法的成员表达式(如 foo.bar, foo['x'])。

核心状态:

  • inMemberExp: 主路径部分
  • inBrackets: 方括号访问
  • inParens: 括号访问
  • inString: 字符串字面量内部

示例:

  • ✅ 合法 → user.name, list[index].value
  • ❌ 非法 → a(), 1user, foo..bar

4. 属性与指令查找:findDir / findProp

这两个函数是 Vue 编译阶段的“节点属性查询器”。

export function findDir(
  node: ElementNode,
  name: string | RegExp,
  allowEmpty: boolean = false,
): DirectiveNode | undefined {
  for (let i = 0; i < node.props.length; i++) {
    const p = node.props[i]
    if (
      p.type === NodeTypes.DIRECTIVE &&
      (allowEmpty || p.exp) &&
      (isString(name) ? p.name === name : name.test(p.name))
    ) {
      return p
    }
  }
}

作用:
快速定位某个指令(如 v-ifv-model)节点。

细节:

  • allowEmpty:是否允许无表达式的指令(如 v-on)。
  • name:支持字符串或正则,用于匹配指令名称。

5. 属性注入机制:injectProp

export function injectProp(
  node: VNodeCall | RenderSlotCall,
  prop: Property,
  context: TransformContext,
): void {
  ...
  if (props == null || isString(props)) {
    propsWithInjection = createObjectExpression([prop])
  } else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
    ...
  } else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
    ...
  } else {
    propsWithInjection = createCallExpression(context.helper(MERGE_PROPS), [
      createObjectExpression([prop]),
      props,
    ])
  }
  ...
}

原理与用途:
在 AST 生成阶段注入新的属性(例如添加 keyref 等虚拟节点属性)。

逻辑说明:

  1. 若当前无 props → 创建一个新对象表达式 { [prop]: value }
  2. 若存在 mergeProps(...) → 在现有参数前追加新属性。
  3. 若存在 toHandlers(...) → 用 MERGE_PROPS 合并。

示例:

<div v-for="i in list" :key="i"></div>

编译后,injectProp 确保 key 存在于最终 createVNode 调用参数中。


三、实践示例

假设我们在模板中写下:

<div v-if="show" class="red" :id="user.id"></div>

编译器在处理时会:

  1. findDir(node, 'if') 定位 v-if 指令;

  2. findProp(node, 'class')findProp(node, 'id', true) 读取属性;

  3. 在生成渲染函数时,通过 injectProp 插入 key

  4. 生成的最终代码大致为:

    createVNode("div", { class: "red", id: user.id, key: 0 })
    

四、拓展与优化

  • 词法解析性能isMemberExpressionBrowser 采用手写状态机而非 AST 解析器,主要是为了性能考虑(编译阶段非常频繁)。
  • SSR 与浏览器分支isMemberExpressionNode 在 Node 环境下用 Babel 解析,以保证 TypeScript 支持。
  • 作用域检测hasScopeRef 用于确定某表达式是否引用了当前上下文变量,在 v-forv-slot 等语义分析中极为关键。

五、潜在问题与思考

  1. 浏览器兼容性问题:正则与 Unicode 字符匹配范围较广,某些极端字符可能导致错误识别。
  2. 递归注入风险injectProp 的嵌套调用路径较深,若处理嵌套 normalizeProps 结构,可能产生意外覆盖。
  3. 性能平衡parseExpression(Babel 调用)比手写解析更安全但更慢,因此 Vue 在浏览器环境默认使用 isMemberExpressionBrowser

六、总结

本文剖析了 Vue 编译器中 utils.ts 的关键逻辑,包括:

  • 静态判断与词法分析;
  • 编译指令查找与属性注入;
  • 作用域与上下文引用检测;
  • 多层工具函数在编译管线中的协作关系。

这些函数虽小,却构成了 Vue 编译器高性能与高鲁棒性的基础。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

🧩 Vue 编译核心:transform.ts 源码深度剖析

作者 excel
2025年11月4日 18:43

一、概念层:什么是「Transform 阶段」?

在 Vue 3 的编译流程中,transform.ts 是核心的 AST 转换阶段(Transform Phase)
它的任务是将模板编译生成的抽象语法树(AST)进行语义增强与结构优化,比如:

  • 把指令(v-ifv-for)转为代码生成节点;
  • 标记哪些节点可以静态提升(hoist);
  • 追踪组件、指令依赖;
  • 为最终代码生成(Codegen)阶段做准备。

👉 总体流程:

template → parse() → transform() → generate()

而本文分析的正是中间的 transform() 逻辑。


二、原理层:整体流程图

export function transform(root: RootNode, options: TransformOptions): void {
  const context = createTransformContext(root, options)
  traverseNode(root, context)
  if (options.hoistStatic) {
    cacheStatic(root, context)
  }
  if (!options.ssr) {
    createRootCodegen(root, context)
  }
  root.helpers = new Set([...context.helpers.keys()])
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = context.imports
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached
  root.transformed = true
}

🧠 原理说明

  1. createTransformContext()

    • 构建整个转换上下文(记录状态、工具函数、缓存)。
    • 内部包含各种集合:helperscomponentshoists 等。
  2. traverseNode()

    • 深度优先遍历整个 AST。
    • 在遍历中执行 nodeTransforms(节点变换器)。
    • 支持插件式扩展。
  3. cacheStatic()

    • 静态节点缓存优化。
  4. createRootCodegen()

    • 将转换完成的 AST 根节点生成最终可被 codegen 处理的结构(VNode 调用)。

三、对比层:Vue 2.x vs Vue 3 Transform 差异

对比点 Vue 2.x Vue 3
AST 表达形式 基于字符串拼接的模板语法树 结构化、类型化的 AST
转换机制 内嵌逻辑、强耦合 插件式 NodeTransform / DirectiveTransform
静态提升 局部静态提升 全局静态提升 + 缓存机制
Helper 管理 全局工具函数 按需导入、依赖追踪
自定义指令编译 内联解析 可扩展的 DirectiveTransform

Vue 3 将编译管线模块化,使每个步骤都具备独立职责、可组合性和扩展性。


四、实践层:核心函数逐段解析

1️⃣ createTransformContext()

export function createTransformContext(
  root: RootNode,
  options: TransformOptions,
): TransformContext {
  const context: TransformContext = {
    filename,
    root,
    helpers: new Map(),
    components: new Set(),
    directives: new Set(),
    hoists: [],
    cached: [],
    // ...
    helper(name) {
      const count = context.helpers.get(name) || 0
      context.helpers.set(name, count + 1)
      return name
    },
    hoist(exp) {
      if (isString(exp)) exp = createSimpleExpression(exp)
      context.hoists.push(exp)
      const identifier = createSimpleExpression(
        `_hoisted_${context.hoists.length}`,
        false,
        exp.loc,
        ConstantTypes.CAN_CACHE,
      )
      identifier.hoisted = exp
      return identifier
    },
  }
  return context
}

🧩 说明

  • 功能:创建整个编译过程的上下文(相当于一个「编译状态机」)。
  • helper() :记录当前使用到的运行时辅助函数(如 createVNodetoDisplayString)。
  • hoist() :实现静态提升,将不变的节点提取为 _hoisted_xx

💬 注释讲解

helper(name) {
  const count = context.helpers.get(name) || 0  // 获取当前 helper 使用次数
  context.helpers.set(name, count + 1)           // 累计引用次数
  return name                                    // 返回 helper symbol
}

2️⃣ traverseNode()

export function traverseNode(node, context) {
  context.currentNode = node
  const { nodeTransforms } = context
  const exitFns = []
  for (let i = 0; i < nodeTransforms.length; i++) {
    const onExit = nodeTransforms[i](node, context)
    if (onExit) exitFns.push(onExit)
  }

  switch (node.type) {
    case NodeTypes.INTERPOLATION:
      context.helper(TO_DISPLAY_STRING)
      break
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context)
      break
  }

  while (exitFns.length) {
    exitFns.pop()()
  }
}

🧩 说明

  • 按照 深度优先遍历 (DFS) 执行节点转换。
  • 每个 NodeTransform 可返回一个 退出函数(Exit Function) ,用于后序清理或反向处理。

💬 注释讲解

// 1. 执行进入阶段 transform
const onExit = nodeTransforms[i](node, context)

// 2. 深度遍历子节点
traverseChildren(node, context)

// 3. 执行退出阶段 transform(类似 Vue 生命周期的 before/after)
exitFns[i]()

3️⃣ createStructuralDirectiveTransform()

export function createStructuralDirectiveTransform(
  name: string | RegExp,
  fn: StructuralDirectiveTransform,
): NodeTransform {
  return (node, context) => {
    if (node.type === NodeTypes.ELEMENT) {
      const exitFns = []
      for (let i = 0; i < node.props.length; i++) {
        const prop = node.props[i]
        if (prop.type === NodeTypes.DIRECTIVE && prop.name === name) {
          node.props.splice(i, 1)
          const onExit = fn(node, prop, context)
          if (onExit) exitFns.push(onExit)
        }
      }
      return exitFns
    }
  }
}

🧩 说明

  • 用于注册「结构性指令」的转换器(如 v-ifv-for)。
  • 它的设计让开发者可以轻松扩展自定义指令编译逻辑。

💬 注释讲解

node.props.splice(i, 1)  
// 移除结构指令,防止重复遍历或死循环

const onExit = fn(node, prop, context)
// 执行自定义转换逻辑(如展开成条件/循环语句)

五、拓展层:插件式编译架构的意义

Vue 3 的 transform 体系带来了极强的可扩展性:

  • 📦 插件式 NodeTransform
    可注册多个节点转换器(如 v-ifv-forv-on 分别实现)。
  • 🔁 多阶段生命周期
    类似 AST 版本的「钩子」机制。
  • 🧱 结构化上下文
    每个阶段可共享编译上下文状态(组件、helper、缓存等)。

这种设计理念与 Babel、Rollup 的插件架构 十分类似,保证了灵活性与可维护性。


六、潜在问题与注意事项

问题点 说明
🔄 节点替换的副作用 replaceNode() 若在不当时机调用可能导致 AST 不一致。
🧮 作用域追踪 addIdentifiers() 逻辑仅在非浏览器构建中执行。
🧩 多层嵌套指令 结构性指令嵌套可能触发多次变换,需要注意执行顺序。
🧰 SSR 模式 ssr 下某些 helper 不应注入(如 TO_DISPLAY_STRING)。

七、结语

transform.ts 是 Vue 编译系统的“中枢神经”,连接了模板解析(Parse)与代码生成(Codegen)。
通过模块化的设计、可插拔的转换器以及完善的上下文机制,Vue 3 实现了强大的模板编译能力和高可扩展性。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

深度解析:Vue 模板编译器中的 Tokenizer 实现原理

作者 excel
2025年11月4日 18:42

一、背景:HTML 与 Vue 模板解析的核心阶段

在 Vue 编译器的前端阶段中,Tokenizer(词法分析器) 是整个解析流程的第一步。
它的任务是将原始字符串(HTML 模板)切分为有意义的词法单元(Tokens) ,供后续的语法分析器(Parser)组装成 AST(抽象语法树)。

Vue 的 Tokenizer 源自 htmlparser2 项目,并在此基础上扩展了:

  • Vue 特有的 插值语法({{}})
  • 指令语法(v-if、:prop、@event 等)
  • SFC 模式(Single File Component) 支持;
  • 对浏览器与 Node 环境的差异化处理(例如 entities 解码器)。

二、原理:状态机驱动的词法分析

整个 Tokenizer 类是一个有限状态机(Finite State Machine) ,用 State 枚举定义了所有可能状态:

export enum State {
  Text = 1,
  InterpolationOpen,
  Interpolation,
  InterpolationClose,
  BeforeTagName,
  InTagName,
  // ...
}

每个状态对应一个处理函数(如 stateTextstateInTagName 等),在 parse() 主循环中被调用:

while (this.index < this.buffer.length) {
  const c = this.buffer.charCodeAt(this.index)
  switch (this.state) {
    case State.Text:
      this.stateText(c)
      break
    case State.InTagName:
      this.stateInTagName(c)
      break
    // ...
  }
  this.index++
}

状态切换机制说明

  • 每当遇到特定字符(如 <>="&),Tokenizer 通过条件判断切换到对应状态

  • 不同状态代表不同上下文(例如 “正在读标签名”、“正在读属性值”、“正在读注释”);

  • 每个状态函数内负责调用回调(callbacks)报告结果,例如:

    this.cbs.onopentagname(start, end)
    this.cbs.onattribdata(start, end)
    this.cbs.oncomment(start, end)
    

三、对比:Tokenizer 在不同模式下的差异

1. 普通 HTML 模式

适用于标准 HTML 结构:

<div class="box">Hello</div>

→ 会被拆解为:

  • <div>onopentagname
  • class="box"onattribname + onattribdata
  • Helloontext
  • </div>onclosetag

2. Vue 模板模式(ParseMode.HTML / BASE)

增加对 Vue 特性处理:

<div v-if="ok" :title="msg">{{ count }}</div>

Tokenizer 识别:

  • {{ count }}oninterpolation
  • v-if / :titleondirname / ondirarg

3. SFC 模式(ParseMode.SFC)

在单文件组件中识别:

  • <template> 内部 HTML;
  • <script><style> 标签视为 RAW Text
  • 自动进入 RCDATA 状态(即不解析内部标签)。

四、实践:运行机制与关键函数详解

1. stateText(c: number)

处理纯文本状态。当遇到 <{{ 时触发状态切换:

private stateText(c: number): void {
  if (c === CharCodes.Lt) {             // '<' → 标签开始
    if (this.index > this.sectionStart)
      this.cbs.ontext(this.sectionStart, this.index)
    this.state = State.BeforeTagName
    this.sectionStart = this.index
  } else if (c === CharCodes.Amp) {     // '&' → 实体解码
    this.startEntity()
  } else if (c === this.delimiterOpen[0]) { // '{{' → 插值表达式
    this.state = State.InterpolationOpen
    this.delimiterIndex = 0
    this.stateInterpolationOpen(c)
  }
}

注释说明

  • sectionStart:当前词段的起始索引;
  • cbs.ontext(...):当文本段结束时回调;
  • delimiterOpen:默认是 {{,Vue 插值符。

2. stateInterpolationOpen / stateInterpolationClose

这两组函数专门为 Vue 的 {{ }} 插值设计:

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
      this.cbs.ontext(this.sectionStart, start)  // 结束上一个文本段
      this.state = State.Interpolation
      this.sectionStart = start
    } else {
      this.delimiterIndex++
    }
  } else {
    this.state = State.Text
    this.stateText(c)
  }
}

→ 当检测到完整的 {{ 后,进入 Interpolation 状态,直到遇到 }}


3. stateInAttrValueDq / stateInAttrValueSq / stateInAttrValueNq

分别处理:

  • 双引号属性:attr="value"
  • 单引号属性:attr='value'
  • 无引号属性:attr=value
private handleInAttrValue(c: number, quote: number) {
  if (c === quote) {
    this.cbs.onattribdata(this.sectionStart, this.index)
    this.cbs.onattribend(QuoteType.Double, this.index + 1)
    this.state = State.BeforeAttrName
  } else if (c === CharCodes.Amp) {
    this.startEntity()  // 处理 &entity;
  }
}

4. startEntity()emitCodePoint()

非浏览器环境下(Node),会借助 entities/lib/decode.js 解码实体字符:

this.entityDecoder = new EntityDecoder(htmlDecodeTree, (cp, consumed) =>
  this.emitCodePoint(cp, consumed),
)

→ 支持 &amp;, &#x27;, &#123; 等。


5. 回调接口(Callbacks)

定义在接口 Callbacks 中,用于与上层解析器通信:

export interface Callbacks {
  ontext(start, end): void
  onopentagname(start, end): void
  onattribdata(start, end): void
  oninterpolation(start, end): void
  oncomment(start, end): void
  onend(): void
  onerr(code: ErrorCodes, index: number): void
}

这使得 Tokenizer 保持纯函数式设计,便于在不同上下文复用。


五、拓展:高性能实现细节

(1) 位运算优化

例如:

(c | 0x20) === this.currentSequence[this.sequenceIndex]

用于快速实现大小写不敏感匹配(ASCII 字母)。

(2) TypedArray 存储

使用 Uint8Array 存储常量序列:

const defaultDelimitersOpen = new Uint8Array([123, 123]) // "{{"

这样比较字符时无需字符串对象转换,大幅优化性能。

(3) 快速跳转机制

fastForwardTo() 用于在不重要的字符区间直接跳跃,提高吞吐:

private fastForwardTo(c: number): boolean {
  while (++this.index < this.buffer.length) {
    if (this.buffer.charCodeAt(this.index) === c)
      return true
  }
  return false
}

六、潜在问题与改进空间

  1. 分支爆炸
    switch (state) 的复杂度较高,可考虑自动状态表生成器(例如 Ragel)。
  2. 错误恢复能力弱
    当前仅部分状态下调用 onerr,可扩展更强的错误恢复逻辑(例如 IDE 语法高亮场景)。
  3. 插值边界条件
    {{{{% 出现时,Vue 的 Interpolation 状态可能被误判,可增加模式标识。
  4. SFC 模式兼容性
    不同 <template lang="..."> 的处理逻辑可进一步模块化(目前集中在 stateInSFCRootTagName)。

七、总结

Vue 的 Tokenizer 是一个高性能、模块化的词法分析器,融合了:

  • HTML 标准语法解析;
  • Vue 模板扩展(指令与插值);
  • Node / 浏览器差异解码;
  • 高效状态机与位操作优化。

它是 Vue 编译器前端的关键基础组件,直接决定了解析器的精度与性能。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

Vue Runtime Helper 常量与注册机制源码解析

作者 excel
2025年11月4日 18:41

本文将从 概念 → 原理 → 对比 → 实践 → 拓展 → 潜在问题 六个层面,详细讲解 Vue 编译器中的运行时 Helper 常量声明与注册逻辑,结合源码进行逐行拆解。


一、概念:什么是 Runtime Helper?

在 Vue 的编译器体系中,Runtime Helper(运行时辅助函数) 是编译器生成的渲染函数在运行时需要依赖的工具。例如,编译器在解析模板时,会将 <div>{{ msg }}</div> 转换为:

createElementVNode("div", null, toDisplayString(msg))

其中 createElementVNodetoDisplayString 就是 runtime helpers
它们在运行时由 Vue 内部的渲染引擎提供,确保渲染函数正常执行。


二、原理:Symbol 唯一标识机制

源码定义如下:

export const FRAGMENT: unique symbol = Symbol(__DEV__ ? `Fragment` : ``)
export const TELEPORT: unique symbol = Symbol(__DEV__ ? `Teleport` : ``)
...

解析与注释:

  1. unique symbol
    TypeScript 的特殊类型,表示这是一个唯一的 symbol 常量,具有更严格的类型约束,用于防止重复定义。
  2. Symbol()
    JavaScript 的原生机制,生成唯一标识符,不会重复冲突,用于在大型代码库中标记运行时 helper。
  3. __DEV__ ? 'name' : ''
    在开发模式下,Symbol 具有可读字符串名,方便调试;
    在生产模式中为空字符串,减少包体积与性能损耗。

例如,FRAGMENT 在开发模式下生成的 Symbol 实际为:

Symbol(Fragment)

而在生产环境则为:

Symbol()

三、对比:Vue 3 与 Vue 2 的差异

特性 Vue 2 Vue 3
Helper 定义 函数式导出,无 Symbol 使用 Symbol 唯一标识
模块引用 直接从 runtime 获取 编译器通过 helperNameMap 映射导入
扩展性 较弱,需手动添加 强化,支持动态注册新 Helper

Vue 3 使用 Symbol 的最大优势在于:

  • 避免命名冲突;
  • 提供类型安全;
  • 支持动态扩展(通过 registerRuntimeHelpers)。

四、实践:helperNameMap 的核心设计

源码片段

export const helperNameMap: Record<symbol, string> = {
  [FRAGMENT]: `Fragment`,
  [TELEPORT]: `Teleport`,
  [SUSPENSE]: `Suspense`,
  ...
}

解释:

  • 该映射表的 key 是 Symbol(唯一标识)
    value 是字符串(runtime 函数名)

编译器在生成代码时,不直接写死函数名,而是通过此映射动态查找对应的导入名称。例如:

import { createVNode, toDisplayString } from "vue"

这部分由 helperNameMap 自动映射生成。


五、拓展:动态注册机制 registerRuntimeHelpers

源码如下:

export function registerRuntimeHelpers(helpers: Record<symbol, string>): void {
  Object.getOwnPropertySymbols(helpers).forEach(s => {
    helperNameMap[s] = helpers[s]
  })
}

逐行解析:

  1. Object.getOwnPropertySymbols(helpers)
    获取传入对象中所有 Symbol 类型的键(因为普通的 Object.keys() 无法获取 Symbol)。
  2. forEach(s => { helperNameMap[s] = helpers[s] })
    将传入的辅助函数映射动态添加到全局 helperNameMap,实现扩展机制。

使用示例:

registerRuntimeHelpers({
  [Symbol('customHelper')]: 'customRuntimeFn'
})

这意味着 Vue 的编译器可以在插件机制中注入新的 helper,支持第三方指令、渲染逻辑等。


六、潜在问题与设计考量

  1. Symbol 无法序列化
    在调试或日志中,Symbol 无法直接输出 JSON,因此需要人工转化或使用 .toString()

  2. 运行时与编译时解耦风险
    如果 helperNameMap 缺少对应的 Symbol 注册,编译器生成的代码在运行时会找不到 helper,导致报错。

  3. 版本兼容问题
    注释中提到:

    /**
     * @deprecated no longer needed in 3.5+
     */
    

    表示部分 helper(如 pushScopeIdpopScopeId)已在新版本中废弃,但保留用于兼容旧版模板编译结果。


七、整体流程图示意

+------------------+
| Template         |
| <div>{{ msg }}</div> |
+------------------+
          |
          v
+------------------+
| Compiler         |
| 生成 helpers:    |
|  createElementVNode |
|  toDisplayString     |
+------------------+
          |
          v
+------------------+
| helperNameMap 映射 |
| { Symbol(...) => 'toDisplayString' } |
+------------------+
          |
          v
+------------------+
| Runtime import   |
| import { toDisplayString } from 'vue' |
+------------------+

八、总结

本文深入剖析了 Vue 编译器运行时辅助函数(Runtime Helpers)的定义、注册与映射机制。
其核心思想在于:

  • Symbol 唯一标识确保类型安全;
  • 映射表提供编译时与运行时的桥梁;
  • 动态注册机制增强插件可扩展性。

这是一种极具可维护性与可扩展性的设计范式,也体现了 Vue 3 在编译器抽象层面的工程思想。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

第五章:辅助函数与全流程整合

作者 excel
2025年11月4日 18:39

🧩 函数 1:condenseWhitespace(nodes)

function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] {
  const shouldCondense = currentOptions.whitespace !== 'preserve'
  let removedWhitespace = false
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]
    if (node.type === NodeTypes.TEXT) {
      if (!inPre) {
        if (isAllWhitespace(node.content)) {
          const prev = nodes[i - 1] && nodes[i - 1].type
          const next = nodes[i + 1] && nodes[i + 1].type
          if (
            !prev ||
            !next ||
            (shouldCondense &&
              ((prev === NodeTypes.COMMENT &&
                (next === NodeTypes.COMMENT || next === NodeTypes.ELEMENT)) ||
                (prev === NodeTypes.ELEMENT &&
                  (next === NodeTypes.COMMENT ||
                    (next === NodeTypes.ELEMENT &&
                      hasNewlineChar(node.content))))))
          ) {
            removedWhitespace = true
            nodes[i] = null as any
          } else {
            node.content = ' '
          }
        } else if (shouldCondense) {
          node.content = condense(node.content)
        }
      } else {
        node.content = node.content.replace(windowsNewlineRE, '\n')
      }
    }
  }
  return removedWhitespace ? nodes.filter(Boolean) : nodes
}

📖 功能说明

统一处理节点中的空白字符(Whitespace),用于压缩模板文本、优化渲染性能。


🔍 逻辑拆解

场景 处理方式
<pre> 内部 保留原样,只将 \r\n 转换为 \n
空白节点(全是空格或换行) 可能被删除或压缩成一个空格 ' '
普通文本中的多余空格 压缩为单空格(调用 condense())。
空白在元素/注释之间 whitespace: "condense" 且存在换行符,则删除节点。

🧠 设计理念

Vue 默认模板渲染不保留多余空格,这是性能优化与 HTML 渲染一致性考虑。
但保留 <pre> 标签原样以尊重语义。


📘 举例

<div>
   Hello
   World
</div>

压缩后等价为:

[  { "type": "TEXT", "content": " Hello World " }]

🧩 函数 2:condense(str)

function condense(str: string) {
  let ret = ''
  let prevCharIsWhitespace = false
  for (let i = 0; i < str.length; i++) {
    if (isWhitespace(str.charCodeAt(i))) {
      if (!prevCharIsWhitespace) {
        ret += ' '
        prevCharIsWhitespace = true
      }
    } else {
      ret += str[i]
      prevCharIsWhitespace = false
    }
  }
  return ret
}

📖 功能说明

将字符串中连续空白压缩为单一空格。

例如:
"foo bar baz""foo bar baz"


💡 原理

利用标志 prevCharIsWhitespace 记录上一个字符是否是空格,
在连续空格时仅保留第一个。


🧩 函数 3:isAllWhitespace(str)hasNewlineChar(str)

function isAllWhitespace(str: string) {
  for (let i = 0; i < str.length; i++) {
    if (!isWhitespace(str.charCodeAt(i))) {
      return false
    }
  }
  return true
}

function hasNewlineChar(str: string) {
  for (let i = 0; i < str.length; i++) {
    const c = str.charCodeAt(i)
    if (c === CharCodes.NewLine || c === CharCodes.CarriageReturn) {
      return true
    }
  }
  return false
}

📖 功能说明

提供文本判断工具:

  • isAllWhitespace() → 判断字符串是否全部为空白;
  • hasNewlineChar() → 判断是否包含换行符。

💡 用途

这两个函数被频繁用于:

  • 空白节点删除;
  • condenseWhitespace() 中的逻辑判断;
  • onCloseTag() 的新行处理。

🧩 函数 4:lookAhead()backTrack()

function lookAhead(index: number, c: number) {
  let i = index
  while (currentInput.charCodeAt(i) !== c && i < currentInput.length - 1) i++
  return i
}

function backTrack(index: number, c: number) {
  let i = index
  while (currentInput.charCodeAt(i) !== c && i >= 0) i--
  return i
}

📖 功能说明

字符扫描工具,用于在字符串中向前或向后搜索特定字符。


📘 举例

  • lookAhead(end, CharCodes.Gt) 用于查找下一个 > 的位置。
  • backTrack(start, CharCodes.Lt) 用于向后回溯到上一个 <(如隐式闭合标签时)。

🧠 设计思路

相比正则表达式,这种“手动扫描”效率更高,也能在解析过程中精确追踪行列位置。


🧩 函数 5:setLocEnd()getLoc()

function setLocEnd(loc: SourceLocation, end: number) {
  loc.end = tokenizer.getPos(end)
  loc.source = getSlice(loc.start.offset, end)
}

📖 功能说明

更新 AST 节点的结束位置信息。
在节点合并、闭合、或文本扩展时调用。


💡 原理

通过 tokenizer 的偏移量转化为源码位置信息(行号、列号、偏移量),
让 AST 的每个节点都带有可追溯的源代码定位。


🧩 函数 6:emitError()

function emitError(code: ErrorCodes, index: number, message?: string) {
  currentOptions.onError(
    createCompilerError(code, getLoc(index, index), undefined, message),
  )
}

📖 功能说明

统一错误报告接口。


🔍 特点

  • 使用 ErrorCodes 枚举控制错误类型;
  • 自动生成位置信息;
  • 调用 onError 回调(默认打印到控制台)。

📘 常见错误码

错误码 描述
X_MISSING_END_TAG 缺少闭合标签
X_INVALID_EXPRESSION 表达式语法错误
EOF_IN_TAG 解析到标签结尾前遇到文件结束

🧩 函数 7:isFragmentTemplate()isComponent()

这些函数我们在上一章讲过,但这里补充一点运行流程背景:

  • isFragmentTemplate()onCloseTag() 时判断 <template> 是否包裹逻辑性指令(v-if/v-for)。
  • isComponent() 在同一位置判断标签是否是组件(根据首字母大写、:is 动态绑定等规则)。

这两个函数是 节点语义分类的最后一步
Vue 编译器正是在这里决定每个标签的“生成代码类型”:
是组件、模板、插槽、还是普通元素。


🧩 函数 8:baseParse() 全流程整合(最终回顾)

export function baseParse(input: string, options?: ParserOptions): RootNode {
  reset()                               // ① 清理状态
  currentInput = input
  currentOptions = extend({}, defaultParserOptions)
  if (options) Object.assign(currentOptions, options)

  tokenizer.mode = ...                  // ② 选择模式 (HTML / SFC / Base)
  tokenizer.inXML = ...
  const delimiters = options?.delimiters
  if (delimiters) {
    tokenizer.delimiterOpen = toCharCodes(delimiters[0])
    tokenizer.delimiterClose = toCharCodes(delimiters[1])
  }

  const root = (currentRoot = createRoot([], input)) // ③ 创建 Root AST
  tokenizer.parse(currentInput)          // ④ 启动状态机解析
  root.loc = getLoc(0, input.length)
  root.children = condenseWhitespace(root.children)
  currentRoot = null
  return root                            // ⑤ 返回 AST
}

🔍 全流程图解

模板字符串
    ↓
[Tokenizer]
  ├── onopentagname() → ElementNode
  ├── onattribname() / ondirname() → Attribute / DirectiveNode
  ├── ontext() → TextNode
  ├── oninterpolation() → InterpolationNode
  ├── onclosetag() → 栈出栈,结构闭合
    ↓
[AST 树生成完毕]condenseWhitespace() → 空白优化
    ↓
RootNode 返回

⚙️ Vue 解析器核心架构总结

模块 主要职责 示例
Tokenizer 词法分析:识别标签、文本、插值 <div>{{ msg }}</div>
Directive Parser 语义解析:识别指令、参数、修饰符 v-if="ok" @click.stop
Expression Parser 语法分析:用 Babel 构建表达式 AST "ok && show"
Tree Builder 维护节点栈,生成层级关系 <div><span></span></div>
Whitespace Optimizer 清理空白与冗余节点 condenseWhitespace()
Error Reporter 统一错误系统与位置追踪 emitError()

💡 运行原理简述

Vue 的模板解析器采用事件驱动状态机 + 栈式构建模型

  • Tokenizer 负责扫描字符流;
  • Parser 监听事件(如 onopentagnameontext);
  • Builder 负责维护栈结构与父子关系;
  • Expression 解析器 负责 Babel AST;
  • Whitespace 处理器 进行语义级压缩。

整个系统具有:

  • ⚙️ 流式解析(无需整句加载);
  • 🔍 位置信息追踪(支持精确错误提示);
  • 🧩 插件式扩展点(支持 SFC、兼容模式)。

🧾 最终总结

Vue 3 的 baseParse() 是一个兼具 编译器严谨性框架灵活性 的模板解析器。
它融合了:

  • HTML 状态机解析
  • Vue 特有语义规则
  • Babel 表达式分析能力
  • 严格的 AST 结构化体系

📚 一句话总结

Vue 的模板解析器,是一台能理解“语义”的 HTML 状态机,它不仅能读懂 <div>,还能明白 v-if{{ msg }} 的意图。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

第四章:表达式与循环解析函数详解

作者 excel
2025年11月4日 18:38

🧩 函数 1:parseForExpression(input: SimpleExpressionNode)

function parseForExpression(
  input: SimpleExpressionNode,
): ForParseResult | undefined {
  const loc = input.loc
  const exp = input.content
  const inMatch = exp.match(forAliasRE)
  if (!inMatch) return

  const [, LHS, RHS] = inMatch

  const createAliasExpression = (
    content: string,
    offset: number,
    asParam = false,
  ) => {
    const start = loc.start.offset + offset
    const end = start + content.length
    return createExp(
      content,
      false,
      getLoc(start, end),
      ConstantTypes.NOT_CONSTANT,
      asParam ? ExpParseMode.Params : ExpParseMode.Normal,
    )
  }

  const result: ForParseResult = {
    source: createAliasExpression(RHS.trim(), exp.indexOf(RHS, LHS.length)),
    value: undefined,
    key: undefined,
    index: undefined,
    finalized: false,
  }

  let valueContent = LHS.trim().replace(stripParensRE, '').trim()
  const trimmedOffset = LHS.indexOf(valueContent)

  const iteratorMatch = valueContent.match(forIteratorRE)
  if (iteratorMatch) {
    valueContent = valueContent.replace(forIteratorRE, '').trim()
    const keyContent = iteratorMatch[1].trim()
    let keyOffset: number | undefined
    if (keyContent) {
      keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
      result.key = createAliasExpression(keyContent, keyOffset, true)
    }

    if (iteratorMatch[2]) {
      const indexContent = iteratorMatch[2].trim()
      if (indexContent) {
        result.index = createAliasExpression(
          indexContent,
          exp.indexOf(indexContent, keyOffset! + keyContent.length),
          true,
        )
      }
    }
  }

  if (valueContent) {
    result.value = createAliasExpression(valueContent, trimmedOffset, true)
  }

  return result
}

📖 功能说明

负责解析 v-for 的表达式,例如:

<li v-for="(item, index) in items"></li>

会被解析为结构化结果:

{
  "source": "items",
  "value": "item",
  "key": "index",
  "index": null
}

🔍 拆解步骤

步骤 说明
提取输入表达式内容,例如 " (item, index) in items "
使用 forAliasRE 正则分离左右两部分(LHS / RHS) → "(item, index)""items"
调用 createAliasExpression() 生成表达式节点
判断 LHS 中是否包含多个变量(item, indexitem, key, idx
分别生成 valuekeyindex 三个变量的 AST 表达式
最终返回 ForParseResult 对象,供上层 v-for 代码生成阶段使用。

🧠 原理剖析

v-for 的解析不仅是语法识别,它还要将表达式的左右两部分正确映射为:

  • 遍历目标source
  • 迭代变量value
  • 可选键名key
  • 可选索引index

这使得编译器在后续生成 render 函数时可以还原成:

_renderList(items, (item, index) => ...)

📘 举例说明

模板表达式 输出结构
v-for="item in list" { value: item, source: list }
v-for="(a, b) in obj" { value: a, key: b, source: obj }
v-for="(x, y, z) in map" { value: x, key: y, index: z, source: map }

🧩 函数 2:createExp(content, isStatic, loc, constType, parseMode)

function createExp(
  content: SimpleExpressionNode['content'],
  isStatic: SimpleExpressionNode['isStatic'] = false,
  loc: SourceLocation,
  constType: ConstantTypes = ConstantTypes.NOT_CONSTANT,
  parseMode = ExpParseMode.Normal,
) {
  const exp = createSimpleExpression(content, isStatic, loc, constType)
  if (
    !__BROWSER__ &&
    !isStatic &&
    currentOptions.prefixIdentifiers &&
    parseMode !== ExpParseMode.Skip &&
    content.trim()
  ) {
    if (isSimpleIdentifier(content)) {
      exp.ast = null // fast path
      return exp
    }
    try {
      const plugins = currentOptions.expressionPlugins
      const options: BabelOptions = {
        plugins: plugins ? [...plugins, 'typescript'] : ['typescript'],
      }
      if (parseMode === ExpParseMode.Statements) {
        exp.ast = parse(` ${content} `, options).program
      } else if (parseMode === ExpParseMode.Params) {
        exp.ast = parseExpression(`(${content})=>{}`, options)
      } else {
        exp.ast = parseExpression(`(${content})`, options)
      }
    } catch (e: any) {
      exp.ast = false
      emitError(ErrorCodes.X_INVALID_EXPRESSION, loc.start.offset, e.message)
    }
  }
  return exp
}

📖 功能说明

将字符串形式的表达式转为 Babel AST 对象(用于进一步分析与静态优化)。


🔍 拆解步骤

步骤 描述
调用 createSimpleExpression() 创建基础表达式节点。
若当前启用了 prefixIdentifiers(即启用作用域变量分析),则尝试使用 Babel 解析表达式。
根据不同模式(普通表达式、参数模式、语句模式)选择解析方式: → parseExpression()parse()
捕获 Babel 抛出的语法错误,通过 emitError() 上报。
返回表达式节点对象。

💡 parseMode 的含义

模式 场景 行为
Normal 普通表达式 (exp)
Params 参数列表(如 v-slot="(a,b)" (exp)=>{}
Statements 多语句(如 v-on="a();b()" parse()
Skip 跳过解析(如 v-for 不做 Babel 处理

📘 举例

输入 解析后结果
ok && show Babel AST: BinaryExpression( "&&" )
[a,b].map(fn) Babel AST: CallExpression
item of list (Skip 模式) 不解析,保持原字符串

🧠 原理讲解

Vue 编译器并不直接执行表达式,而是使用 Babel 进行静态语法树构建,这样可以实现:

  • 静态分析(判断是否常量);
  • 标识符前缀化(作用域隔离);
  • 代码优化(提前折叠常量)。

🧩 函数 3:dirToAttr(dir: DirectiveNode)

function dirToAttr(dir: DirectiveNode): AttributeNode {
  const attr: AttributeNode = {
    type: NodeTypes.ATTRIBUTE,
    name: dir.rawName!,
    nameLoc: getLoc(
      dir.loc.start.offset,
      dir.loc.start.offset + dir.rawName!.length,
    ),
    value: undefined,
    loc: dir.loc,
  }
  if (dir.exp) {
    const loc = dir.exp.loc
    if (loc.end.offset < dir.loc.end.offset) {
      loc.start.offset--
      loc.end.offset++
    }
    attr.value = {
      type: NodeTypes.TEXT,
      content: (dir.exp as SimpleExpressionNode).content,
      loc,
    }
  }
  return attr
}

📖 功能说明

将指令(DirectiveNode)转换为普通属性节点(AttributeNode)。
用于 v-pre 等场景下“退化处理”。


📘 举例

v-bind:id="'foo'"v-pre 模式下会被视为:

{
  "type": "ATTRIBUTE",
  "name": "v-bind:id",
  "value": "'foo'"
}

💡 应用场景

  • v-pre:关闭指令编译;
  • 模板兼容模式(老 Vue2 模板中保留 v- 指令原样)。

🧩 函数 4:isFragmentTemplate(el)

const specialTemplateDir = new Set(['if', 'else', 'else-if', 'for', 'slot'])
function isFragmentTemplate({ tag, props }: ElementNode): boolean {
  if (tag === 'template') {
    for (let i = 0; i < props.length; i++) {
      if (props[i].type === NodeTypes.DIRECTIVE &&
          specialTemplateDir.has((props[i] as DirectiveNode).name)) {
        return true
      }
    }
  }
  return false
}

📖 功能说明

判断 <template> 是否是“片段模板”,如 v-ifv-forv-slot 等控制结构模板。


📘 举例

<template v-if="ok">...</template>

→ 会被标识为 Fragment Template,特殊对待(不会生成真实 DOM 节点)。


🧩 函数 5:isComponent(el)

function isComponent({ tag, props }: ElementNode): boolean {
  if (currentOptions.isCustomElement(tag)) return false
  if (
    tag === 'component' ||
    isUpperCase(tag.charCodeAt(0)) ||
    isCoreComponent(tag) ||
    (currentOptions.isBuiltInComponent &&
      currentOptions.isBuiltInComponent(tag)) ||
    (currentOptions.isNativeTag && !currentOptions.isNativeTag(tag))
  ) {
    return true
  }
  ...
  return false
}

📖 功能说明

识别当前标签是否是 Vue 组件。


🔍 判定规则

条件 示例 判断结果
自定义元素 <my-element> ❌(由浏览器识别)
<component> 标签
首字母大写 <HelloWorld>
核心组件 <Transition>
内建组件 <KeepAlive>
使用 :is 动态绑定组件 <div :is="'Custom'"> ✅(兼容模式下)

🧠 原理

Vue 使用首字母大写规则 + 注册信息判断节点语义类型,这样 <div> 不会被视为组件,而 <MyDiv> 会。


🧩 函数 6:ExpParseMode 枚举

enum ExpParseMode {
  Normal,
  Params,
  Statements,
  Skip,
}
模式 用途 解析方式
Normal 通用表达式 包裹在 ()
Params 参数上下文(如 slot 作用域) 包裹成箭头函数
Statements 多语句(如 v-on 内多表达式) 交给 Babel.parse()
Skip 跳过 Babel 解析(如 v-for) 保留字符串

✅ 本章总结

本章讲解了 Vue 解析器最“智能”的部分:

它不仅能识别语法结构,还能理解表达式含义,并用 Babel 进行结构化语法树分析。

函数 作用
parseForExpression 解析 v-for 表达式结构
createExp 构造表达式 AST(支持模式化解析)
dirToAttr 将指令退化为属性(用于 v-pre)
isFragmentTemplate 判断模板片段是否语义化结构
isComponent 智能识别组件节点类型

📘 下一章预告(第五章)
我们将讲解最后的“辅助与优化函数”,包括:

  • 空白管理(condenseWhitespacecondense
  • 节点位置控制(setLocEndgetLoc
  • 错误恢复机制
  • 命名空间与 XML 模式控制

并在最后总结整个 Vue 解析器的架构逻辑与运行模型。

第三章:指令与属性解析函数组详解

作者 excel
2025年11月4日 18:37

🧩 函数 1:oninterpolation(start, end)

oninterpolation(start, end) {
  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)
  if (exp.includes('&')) {
    exp = __BROWSER__ ? currentOptions.decodeEntities!(exp, false) : decodeHTML(exp)
  }
  addNode({
    type: NodeTypes.INTERPOLATION,
    content: createExp(exp, false, getLoc(innerStart, innerEnd)),
    loc: getLoc(start, end),
  })
}

📖 功能说明

解析插值表达式 {{ ... }},生成 INTERPOLATION 节点。


🔍 拆解步骤

步骤 描述
若当前处于 v-pre 区域(跳过编译),则直接当作文本处理。
计算插值内表达式的真实起止位置。
去除前后空白字符。
若表达式中包含 HTML 实体,则解码(如 &lt;<)。
调用 createExp() 构建表达式节点。
调用 addNode() 插入到当前父节点的 children 中。

🧠 设计原理

createExp() 会尝试将 {{ message }} 转换为:

{
  "type": 5,
  "content": { "type": 4, "content": "message", "isStatic": false }
}

其中:

  • NodeType 5 表示插值;
  • NodeType 4 表示简单表达式。

🧩 函数 2:ondirname(start, end)

ondirname(start, end) {
  const raw = getSlice(start, end)
  const name =
    raw === '.' ? 'bind' :
    raw === ':' ? 'bind' :
    raw === '@' ? 'on' :
    raw === '#' ? 'slot' :
    raw.slice(2)
  ...
}

📖 功能说明

解析指令的名称部分,比如 v-ifv-forv-bind@click:src


🔍 拆解步骤

步骤 内容
从模板源中提取原始指令标识(v-:@ 等)。
根据首字符映射到规范化名称:
  • :v-bind
  • @v-on
  • #v-slot
  • .v-bind.prop |
    | | 若当前在 v-pre 模式或指令名为空,则退化为普通属性。 |
    | | 否则创建 DirectiveNode,初始化指令名、参数、修饰符数组。 |
    | | 特殊处理 v-pre:进入原样渲染模式,记录当前节点边界。 |

📘 举例

<div :id="uid" @click="doSomething" v-if="ok"></div>

对应三个指令节点:

[  { "name": "bind", "arg": "id", "exp": "uid" },  { "name": "on", "arg": "click", "exp": "doSomething" },  { "name": "if", "exp": "ok" }]

🧩 函数 3:ondirarg(start, end)

ondirarg(start, end) {
  if (start === end) return
  const arg = getSlice(start, end)
  if (inVPre && !isVPre(currentProp!)) {
    (currentProp as AttributeNode).name += arg
    setLocEnd((currentProp as AttributeNode).nameLoc, end)
  } else {
    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,
    )
  }
}

📖 功能说明

解析指令参数,例如:

<div :id="foo"></div>
<div v-on:[event]="handler"></div>

中的 id[event]


🧠 原理

Vue 支持两种参数类型:

  • 静态参数:直接字符串,如 :id
  • 动态参数:使用方括号包裹,如 v-bind:[attr]

ondirarg 会判断是否为动态参数,并调用 createExp() 生成对应的表达式节点。


🧩 函数 4:ondirmodifier(start, end)

ondirmodifier(start, end) {
  const mod = getSlice(start, end)
  if (inVPre && !isVPre(currentProp!)) {
    (currentProp as AttributeNode).name += '.' + mod
    setLocEnd((currentProp as AttributeNode).nameLoc, end)
  } else if ((currentProp as DirectiveNode).name === 'slot') {
    const arg = (currentProp as DirectiveNode).arg
    if (arg) {
      (arg as SimpleExpressionNode).content += '.' + mod
      setLocEnd(arg.loc, end)
    }
  } else {
    const exp = createSimpleExpression(mod, true, getLoc(start, end))
    (currentProp as DirectiveNode).modifiers.push(exp)
  }
}

📖 功能说明

解析指令修饰符(如 .stop.sync.prevent 等)。


🔍 举例

<button @click.stop.prevent="doSomething"></button>

生成:

{
  "name": "on",
  "arg": "click",
  "modifiers": ["stop", "prevent"]
}

💡 特殊情况

v-slot 指令的修饰符实际是作用在参数上的,因此需要特殊拼接处理。


🧩 函数 5:onattribname(start, end)

onattribname(start, end) {
  currentProp = {
    type: NodeTypes.ATTRIBUTE,
    name: getSlice(start, end),
    nameLoc: getLoc(start, end),
    value: undefined,
    loc: getLoc(start),
  }
}

📖 功能说明

解析普通属性名(非指令),如:

<img src="logo.png" alt="Logo">

生成:

{ "type": "ATTRIBUTE", "name": "src", "value": "logo.png" }

🧩 函数 6:onattribdata(start, end)onattribend(quote, end)

(a)onattribdata

onattribdata(start, end) {
  currentAttrValue += getSlice(start, end)
  if (currentAttrStartIndex < 0) currentAttrStartIndex = start
  currentAttrEndIndex = end
}

逐步累积属性值(例如处理 src="lo...go.png" 时,分多次调用)。


(b)onattribend

onattribend(quote, end) {
  if (currentOpenTag && currentProp) {
    setLocEnd(currentProp.loc, end)
    if (quote !== QuoteType.NoValue) {
      if (__BROWSER__ && currentAttrValue.includes('&')) {
        currentAttrValue = currentOptions.decodeEntities!(currentAttrValue, true)
      }
      if (currentProp.type === NodeTypes.ATTRIBUTE) {
        if (currentProp.name === 'class') {
          currentAttrValue = condense(currentAttrValue).trim()
        }
        currentProp.value = {
          type: NodeTypes.TEXT,
          content: currentAttrValue,
          loc: getLoc(currentAttrStartIndex, currentAttrEndIndex)
        }
      } else {
        currentProp.exp = createExp(currentAttrValue, false, getLoc(currentAttrStartIndex, currentAttrEndIndex))
      }
    }
    currentOpenTag.props.push(currentProp)
  }
  currentAttrValue = ''
  currentAttrStartIndex = currentAttrEndIndex = -1
}

📖 功能说明

当属性或指令值结束时被调用,用于创建 value 节点或指令表达式。


💡 特殊逻辑

  • class 属性做空格压缩;
  • 对空值发出错误提示;
  • v-forv-on 等指令调用 createExp() 生成 Babel AST;
  • 检查 .sync 修饰符以兼容 Vue 2。

🧩 函数 7:oncomment(start, end)

oncomment(start, end) {
  if (currentOptions.comments) {
    addNode({
      type: NodeTypes.COMMENT,
      content: getSlice(start, end),
      loc: getLoc(start - 4, end + 3),
    })
  }
}

📖 功能说明

解析注释节点(<!-- comment -->),在启用 comments: true 时保留。


🧩 函数 8:ondir* 系列小结

函数 作用 典型输入 输出类型
ondirname 解析指令名 v-if:@ DirectiveNode
ondirarg 解析参数 [attr]click SimpleExpressionNode
ondirmodifier 解析修饰符 .stop.sync 数组元素
onattribname 解析属性名 classsrc AttributeNode
onattribend 解析属性值 "foo" TextNode or ExpressionNode

✅ 本章总结

这一章展示了 Vue 模板解析器最关键的部分——语法语义识别

  • 通过回调组合,实现类 HTML 语法的“增量解析”;
  • 支持动态参数、修饰符与内嵌表达式;
  • 自动区分指令与普通属性;
  • 兼容 Vue 2.x 行为。

Vue 的模板语法并不是简单的 HTML 扩展,而是通过词法状态机 + 指令语义层的组合机制精确实现的。


📘 下一章预告(第四章)
我们将讲解更底层的表达式与循环结构解析逻辑,包括:

  • parseForExpression()(v-for 拆解)
  • createExp()(表达式 AST 生成)
  • dirToAttr()(指令转属性)
  • isComponent()isFragmentTemplate()(组件识别)

第二章:标签与文本节点解析函数组详解

作者 excel
2025年11月4日 18:36

🧩 函数 1:onText(content, start, end)

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 实体(如 &lt;),则调用 decodeEntities 进行解码。
找出当前节点的父级(stack[0] 表示当前打开的标签节点,否则为根节点)。
检查上一个子节点是否也是文本节点,如果是则合并文本内容。
否则新建一个 TEXT 类型节点,推入 children 数组。

🧠 原理

Vue 编译器会尽量合并相邻文本节点,以减少虚拟 DOM 节点数量。例如:

<div>hello {{ name }}</div>

将被解析为单一文本节点:

TEXT: "hello " + INTERPOLATION("name")

📌 小结

✅ 解码 HTML 实体
✅ 合并连续文本
✅ 保留精确位置(getLoc()


🧩 函数 2:endOpenTag(end)

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)来维护嵌套标签:

<div>
  <span></span>
</div>

解析 <div> 时入栈,解析到 </div> 时出栈。
每个节点的层级关系由 stack 决定。


🧩 函数 3:onCloseTag(el, end, isImplied = false)

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-ifv-fortemplate 的语法兼容性。

🧠 原理讲解

Vue 解析器的闭合逻辑非常严谨,它不仅仅做结构配对,还会:

  • 根据标签与属性自动推断节点语义类型
  • 检查标签未闭合、错误嵌套
  • 支持v-pre、v-if、v-for 等特殊行为恢复

🧩 函数 4:addNode(node)

function addNode(node: TemplateChildNode) {
  (stack[0] || currentRoot).children.push(node)
}

📖 功能说明

将一个新节点(文本 / 注释 / 元素)追加到当前父节点的 children 中。


💡 原理

  • 如果栈中有元素 → 当前节点是栈顶元素的子节点;
  • 如果栈为空 → 当前节点属于根节点。

这就是 Vue AST 树的层级生成方式。


🧩 函数 5:lookAhead(index, c) & backTrack(index, c)

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)

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)

function isAllWhitespace(str: string) { ... }
function hasNewlineChar(str: string) { ... }

📖 功能说明

辅助判断函数,用于空白符检测与换行判断。


💡 应用

condenseWhitespaceonCloseTag 等处控制换行与文本清理逻辑。


🧩 函数 8:condense(str)

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 形状。


📘 下一章预告
第三章我们将进入更复杂的解析逻辑:

🔹 指令解析(ondirnameondirargondirmodifier
🔹 属性与值解析(onattribdataonattribend
🔹 插值解析(oninterpolation

这些函数涉及 Vue 模板语法的灵魂部分(v-if、v-for、v-bind、{{ }} 等)。

📘 Vue 3 模板解析器源码精讲(baseParse.ts)

作者 excel
2025年11月4日 18:35

🧩 总体结构概览

这个文件的职责是将模板字符串解析为抽象语法树(AST)。
整体由三层结构组成:

层级 内容 作用
外层定义 类型声明、常量、状态变量 管理解析过程中的全局状态
核心函数组 解析、节点生成、错误处理 负责模板解析逻辑
辅助函数组 工具类,如 getLoccondenseWhitespace 辅助主流程的定位与优化

第一部分:全局状态与初始化逻辑


### 1️⃣ reset()

function reset() {
  tokenizer.reset()
  currentOpenTag = null
  currentProp = null
  currentAttrValue = ''
  currentAttrStartIndex = -1
  currentAttrEndIndex = -1
  stack.length = 0
}

📖 功能说明

清空所有解析状态,用于开始一次新的模板解析任务。

🔍 逻辑拆解

步骤 操作 说明
tokenizer.reset() 重置状态机(清空缓冲、位置指针)
清空当前正在解析的标签 (currentOpenTag) 避免上次解析的残留
清空属性状态 (currentPropcurrentAttrValue) 防止多属性串联错误
重置索引标记 用于属性值的精确定位
清空标签栈 重新开始解析树结构

💡 原理

解析器是状态机驱动的;每次调用 baseParse() 之前必须保证状态干净。
若不清理状态,标签嵌套关系会混乱,导致 AST 错误。


### 2️⃣ baseParse(input, options?)

export function baseParse(input: string, options?: ParserOptions): RootNode {
  reset()
  currentInput = input
  currentOptions = extend({}, defaultParserOptions)

  if (options) {
    for (let key in options) {
      if (options[key] != null) currentOptions[key] = options[key]
    }
  }
  ...
  const root = (currentRoot = createRoot([], input))
  tokenizer.parse(currentInput)
  root.loc = getLoc(0, input.length)
  root.children = condenseWhitespace(root.children)
  currentRoot = null
  return root
}

📖 功能说明

baseParse 是整个文件的主函数
负责从模板字符串生成 AST 树的根节点RootNode)。


🔍 逻辑拆解

步骤 描述
① 调用 reset() 清空上一次解析状态
② 保存输入模板 存入 currentInput
③ 合并解析选项 使用 extend 结合用户自定义配置与默认值
④ 初始化解析模式 根据 parseMode 确定 HTML / SFC / Base 模式
⑤ 初始化 tokenizer 设置分隔符、命名空间、XML 模式
⑥ 创建根节点 createRoot([], input)
⑦ 启动解析 tokenizer.parse(currentInput)(核心状态机驱动)
⑧ 修正位置与空白 调用 getLoccondenseWhitespace
⑨ 返回完整的 AST 含子节点、位置信息等

🧠 原理说明

Vue 使用 事件驱动解析模型
tokenizer 扫描模板时,会触发一系列回调(如 onopentagnameontext 等),这些回调逐步组装出 AST 节点。


🧩 拓展示例

baseParse('<div v-if="ok">{{ msg }}</div>')

➡ 输出的 RootNode 类似于:

{
  "type": 0,
  "children": [
    {
      "type": 1,
      "tag": "div",
      "props": [
        { "type": 7, "name": "if", "exp": { "content": "ok" } }
      ],
      "children": [
        { "type": 5, "content": { "content": "msg" } }
      ]
    }
  ]
}

### 3️⃣ emitError()

function emitError(code: ErrorCodes, index: number, message?: string) {
  currentOptions.onError(
    createCompilerError(code, getLoc(index, index), undefined, message),
  )
}

📖 功能说明

统一的错误上报函数。负责将解析阶段的错误格式化为 CompilerError 对象并交给回调。

🔍 逻辑拆解

步骤 内容
调用 createCompilerError 构造错误对象
使用 getLoc() 精确标出错误发生的位置
调用配置的 onError 回调进行处理(默认:打印警告)

💡 设计思路

通过错误码系统(ErrorCodes),Vue 可以在编译时快速定位语法错误,如:

  • 缺少闭合标签;
  • 不合法的插值表达式;
  • 指令名拼写错误等。

### 4️⃣ getSlice(start, end)

function getSlice(start: number, end: number) {
  return currentInput.slice(start, end)
}

📖 功能说明

从源字符串中截取对应区间的内容。

🧩 应用场景

用于生成 AST 节点的 sourceloc.source 信息,保证定位准确。


### 5️⃣ getLoc(start, end?)

function getLoc(start: number, end?: number): SourceLocation {
  return {
    start: tokenizer.getPos(start),
    end: end == null ? end : tokenizer.getPos(end),
    source: end == null ? end : getSlice(start, end),
  }
}

📖 功能说明

生成一个 SourceLocation 对象,表示源代码位置范围(用于调试与错误提示)。

💡 原理

tokenizer.getPos() 会将字符偏移量转换为:

{ offset: 10, line: 1, column: 11 }

这样,Vue 编译错误可以指明“出错的行列位置”。


### 6️⃣ setLocEnd()

function setLocEnd(loc: SourceLocation, end: number) {
  loc.end = tokenizer.getPos(end)
  loc.source = getSlice(loc.start.offset, end)
}

📖 功能说明

更新现有位置对象的结束坐标(用于文本合并或标签闭合时)。


### 7️⃣ cloneLoc()

export function cloneLoc(loc: SourceLocation): SourceLocation {
  return getLoc(loc.start.offset, loc.end.offset)
}

📖 功能说明

复制一个 SourceLocation(防止引用同一对象造成修改污染)。


✨ 下一部分预告

上面我们讲完了基础状态管理与定位逻辑。
接下来第二章将进入 标签与文本节点解析函数组,包括:

  • onText()
  • onCloseTag()
  • endOpenTag()
  • addNode()
  • condenseWhitespace()
  • 等一系列高频调用函数。

这些函数负责 AST 的结构构建与白名单清理,是 Vue 模板语法语义识别的核心。

Vue 模板编译器核心选项解析:从 Parser 到 Codegen 的全链路设计

作者 excel
2025年11月4日 18:32

本文深入分析了 Vue 编译器(compiler-core)中的核心类型定义文件。通过这些接口与类型,我们可以了解模板从 解析(Parse) → 转换(Transform) → 代码生成(Codegen) 的整个生命周期设计理念。


一、概念篇:CompilerOptions 的多层结构

Vue 模板编译器的配置项由多个阶段组成:

  • ParserOptions:负责模板语法解析;
  • TransformOptions:负责 AST(抽象语法树)转换;
  • CodegenOptions:负责最终代码生成;
  • 三者最终合并为 CompilerOptions

在源码中:

export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions

即:编译器配置是一个由三层选项叠加的组合体。


二、原理篇:三阶段编译管线(Pipeline)

Vue 模板编译器的核心思路是“管线式”处理:

  1. Parsing 阶段
    输入:原始模板字符串。
    输出:AST(抽象语法树)。
    关键接口:ParserOptions
  2. Transform 阶段
    输入:AST。
    输出:优化后的 AST。
    关键接口:TransformOptions
  3. Codegen 阶段
    输入:AST。
    输出:渲染函数(render())。
    关键接口:CodegenOptions

这种分层设计的优势是:每一层都可以独立扩展或替换,从而实现不同平台(如 DOM、SSR、自定义渲染器)的编译。


三、对比篇:Vue 编译器与 Babel、Svelte 的结构异同

对比维度 Vue Compiler Babel Svelte Compiler
模板输入 HTML + 指令语法 JS/TS 源码 HTML + 内联逻辑
AST 层级 自定义 AST + Babel AST Babel AST 自定义 AST
插件机制 NodeTransform / DirectiveTransform Babel Plugin Compiler Hook
输出形式 渲染函数(JS) 新的 JS 代码 JS + 运行时代码

Vue 采用“双 AST 系统”(模板 AST + 表达式 AST),其中表达式部分交由 Babel 解析,这就是 expressionPlugins 存在的意义。


四、实践篇:核心类型逐段解析与示例

1. ParserOptions — 模板解析阶段

定义模板解析行为,例如如何处理标签、空白、实体编码等。

export interface ParserOptions extends ErrorHandlingOptions, CompilerCompatOptions {
  parseMode?: 'base' | 'html' | 'sfc'
  isNativeTag?: (tag: string) => boolean
  isVoidTag?: (tag: string) => boolean
  delimiters?: [string, string]
  whitespace?: 'preserve' | 'condense'
  comments?: boolean
}

注释解读:

  • parseMode:控制解析模式,支持纯 HTML 模式、SFC 模式等;
  • isVoidTag:用于识别 <img><br> 等自闭合标签;
  • delimiters:定义插值语法的边界(默认 {{ }});
  • whitespace:定义空白处理策略。

示例:

const parserOptions: ParserOptions = {
  parseMode: 'html',
  isVoidTag: tag => ['img', 'br', 'hr'].includes(tag),
  whitespace: 'condense',
  comments: true
}

2. TransformOptions — AST 转换阶段

控制编译时的 AST 优化与转换行为。

export interface TransformOptions extends SharedTransformCodegenOptions {
  nodeTransforms?: NodeTransform[]
  directiveTransforms?: Record<string, DirectiveTransform | undefined>
  hoistStatic?: boolean
  cacheHandlers?: boolean
  hmr?: boolean
}

注释解读:

  • nodeTransforms:节点级转换插件(如处理 v-ifv-for);
  • directiveTransforms:指令转换插件(如 v-modelv-bind);
  • hoistStatic:开启静态提升优化;
  • cacheHandlers:缓存事件处理函数以减少运行时开销;
  • hmr:为热更新生成兼容代码。

示例:

const transformOptions: TransformOptions = {
  hoistStatic: true,
  nodeTransforms: [transformElement, transformText],
  directiveTransforms: { model: transformModel }
}

3. CodegenOptions — 渲染代码生成阶段

控制最终生成渲染函数的输出形态。

export interface CodegenOptions extends SharedTransformCodegenOptions {
  mode?: 'module' | 'function'
  sourceMap?: boolean
  optimizeImports?: boolean
  runtimeModuleName?: string
}

注释解读:

  • mode

    • 'module' → 输出 ES 模块形式;
    • 'function' → 输出普通函数(适合浏览器运行时编译)。
  • runtimeModuleName:控制运行时 helper 引入源(默认 'vue')。

示例:

const codegenOptions: CodegenOptions = {
  mode: 'module',
  sourceMap: true,
  runtimeModuleName: 'vue'
}

五、拓展篇:BindingTypes 与脚本上下文推断

Vue 编译器还引入了 BindingTypes 枚举,用于分析 <script setup> 中变量的绑定类型。

export enum BindingTypes {
  DATA = 'data',
  PROPS = 'props',
  SETUP_REF = 'setup-ref',
  SETUP_CONST = 'setup-const',
  LITERAL_CONST = 'literal-const'
}

它在运行时起到关键作用,帮助编译器决定哪些变量需要 unref(),哪些可以直接访问。例如:

<script setup>
const count = ref(0) // -> BindingTypes.SETUP_REF
const title = 'Hello' // -> BindingTypes.LITERAL_CONST
</script>

六、潜在问题篇:定制化编译的风险与兼容性考量

  1. 自定义解析器冲突
    过度修改 isNativeTaggetNamespace 可能导致 AST 不兼容。
  2. 表达式解析风险
    若自定义 expressionPlugins,必须与 Vue 内部 Babel 配置兼容,否则会出现解析错误。
  3. HMR 与 SSR 不一致问题
    hmr: truessr: true 并存时,生成逻辑分支复杂,需注意差异性。
  4. 绑定元数据失效
    在未正确分析 <script setup> 的情况下,bindingMetadata 缺失会导致模板优化失败。

七、总结:编译器的可扩展与平台适配性

Vue 的编译器并非固定的“黑盒”,而是通过类型定义体现了其“插件式架构”哲学:

  • ParserOptions 负责语法层;
  • TransformOptions 负责语义层;
  • CodegenOptions 负责输出层。

这种分层设计让 Vue 能无缝支持:

  • Web 平台;
  • SSR;
  • 自定义渲染目标(如 Native、Canvas、Mini Program)。

结语:
通过这份类型定义文件,我们不仅看到 Vue 模板编译器的底层设计,还能理解其生态可扩展性的根源 —— 所有编译行为皆由配置与插件驱动。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

Vue 编译器核心模块结构与导出机制详解

作者 excel
2025年11月4日 18:31

本文将深入解析 Vue 编译器核心模块的导出逻辑。源码来自 Vue 的编译器核心部分(如 @vue/compiler-core),这部分代码主要负责模板编译的核心流程:解析(parse)→ 转换(transform)→ 代码生成(codegen)
我们将逐层拆解 index.ts 中的导出结构,理解其设计思路与模块关系。


一、背景:Vue 编译器的模块化设计

Vue 的编译器(compiler-core)被拆解成多个功能层:

  1. 解析(Parser) :将模板字符串转化为抽象语法树(AST)。
  2. 转换(Transform) :对 AST 进行语义转换、指令处理与优化。
  3. 代码生成(Codegen) :将最终的 AST 转为可执行的渲染函数(JavaScript 代码)。
  4. 错误处理(Errors) :提供统一的错误类型、错误码与提示。
  5. 兼容性处理(Compat) :提供 v2 → v3 的过渡辅助。

二、源码主体结构

export { baseCompile } from './compile'

🔍 说明:

  • baseCompile 是编译流程的核心入口函数。
  • 内部通常会调用 baseParsetransformgenerate,形成完整的编译管线。
  • 在 Vue 中,compileToFunction() 便基于 baseCompile 封装而成。

三、编译选项与类型导出

export {
  type CompilerOptions,
  type ParserOptions,
  type TransformOptions,
  type CodegenOptions,
  type HoistTransform,
  type BindingMetadata,
  BindingTypes,
} from './options'

🔍 说明:

  • 这部分统一导出编译过程的配置类型与绑定类型常量:

    • CompilerOptions:控制整个编译过程的全局选项。
    • ParserOptions:定义模板解析的细节。
    • TransformOptions:定义转换阶段的策略。
    • CodegenOptions:控制生成代码的风格与优化策略。
    • BindingMetadata / BindingTypes:用于描述模板中绑定(如 v-bind、变量作用域等)的类型信息。

这些类型广泛用于 Vue 的运行时与插件生态中,确保编译器行为的可配置性与类型安全。


四、解析阶段(Parser)

export { baseParse } from './parser'

🔍 说明:

  • baseParse 将模板字符串解析为 AST(抽象语法树)

  • 解析的核心逻辑包括:

    • 模板标签与属性识别;
    • 指令(如 v-ifv-for)的结构化;
    • 文本与插值节点的构造。

解析结果通常形如:

{
  type: NodeTypes.ROOT,
  children: [
    { type: NodeTypes.ELEMENT, tag: 'div', children: [...] }
  ]
}

五、转换阶段(Transform)

export {
  transform,
  type TransformContext,
  createTransformContext,
  traverseNode,
  createStructuralDirectiveTransform,
  type NodeTransform,
  type StructuralDirectiveTransform,
  type DirectiveTransform,
} from './transform'

🔍 说明:

该部分是 Vue 编译器的“中枢”:

  • transform() :主入口,遍历并转换 AST。
  • createTransformContext() :为每次转换创建上下文对象,存储作用域、helpers、imports 等。
  • traverseNode() :递归遍历 AST 节点。
  • createStructuralDirectiveTransform() :用于注册结构性指令(如 v-ifv-for)的转换函数。

转换阶段是“模板语法” → “渲染逻辑”的关键环节。
例如:
<div v-if="ok">Hello</div>
会被转换为条件表达式结构:
ok ? createElementBlock('div', null, 'Hello') : null


六、代码生成阶段(Codegen)

export {
  generate,
  type CodegenContext,
  type CodegenResult,
  type CodegenSourceMapGenerator,
  type RawSourceMap,
} from './codegen'

🔍 说明:

  • generate() :将最终 AST 转换为渲染函数源码字符串。

  • 其输出包括:

    • 渲染函数代码;
    • SourceMap;
    • Helper 引用列表。

代码生成结果示例:

function render(_ctx, _cache) {
  return _ctx.ok ? (_openBlock(), _createElementBlock("div", null, "Hello")) : _createCommentVNode("v-if");
}

七、错误处理模块(Errors)

export {
  ErrorCodes,
  errorMessages,
  createCompilerError,
  type CoreCompilerError,
  type CompilerError,
} from './errors'

🔍 说明:

  • 提供统一的错误系统:

    • ErrorCodes:定义标准化的错误码;
    • createCompilerError():用于在编译阶段创建结构化错误;
    • errorMessages:用于映射错误信息。

这样设计的好处是,Vue 的 IDE 插件或开发者工具可精准定位编译错误。


八、AST 与工具函数

export * from './ast'
export * from './utils'
export * from './babelUtils'
export * from './runtimeHelpers'

🔍 说明:

这些模块提供底层基础设施:

  • ast:节点定义、类型枚举;
  • utils:通用编译辅助函数;
  • babelUtils:与 Babel AST 兼容的工具;
  • runtimeHelpers:定义运行时依赖的 helper 函数标识。

九、模板语法专用转换模块

export { transformModel } from './transforms/vModel'
export { transformOn } from './transforms/vOn'
export { transformBind } from './transforms/vBind'
export { processIf } from './transforms/vIf'
export { processFor, createForLoopParams } from './transforms/vFor'
export {
  transformExpression,
  processExpression,
  stringifyExpression,
} from './transforms/transformExpression'

🔍 说明:

这些是针对 Vue 模板指令的专用转换函数:

指令 对应函数 功能
v-model transformModel 建立双向绑定语义
v-on transformOn 处理事件绑定
v-bind transformBind 处理动态属性绑定
v-if processIf 条件渲染结构转换
v-for processFor 循环结构转换
表达式 transformExpression 处理模板插值与作用域表达式

这些转换函数共同组成了 Vue 编译器的“语法插件系统”。


十、兼容性支持模块(v2 → v3)

export {
  checkCompatEnabled,
  warnDeprecation,
  CompilerDeprecationTypes,
} from './compat/compatConfig'

🔍 说明:

  • 提供兼容性检测与弃用警告机制;
  • 帮助 Vue 2 项目平滑迁移至 Vue 3;
  • 例如检测到 filtersinline-template 等旧语法时,会通过 warnDeprecation() 发出提示。

十一、总结:模块协作关系

下图描述了核心模块的协作关系(概念层级):

[parse][transform][codegen]
   ↓           ↓            ↓
 AST      指令转换       生成渲染函数

整体的执行流程:

function baseCompile(template, options) {
  const ast = baseParse(template, options)
  transform(ast, options)
  return generate(ast, options)
}

十二、拓展与潜在问题

🔧 拓展方向:

  • 可在 transform 阶段注入自定义指令转换器,实现 DSL(领域特定语言)模板;
  • 可定制 CodegenOptions 以支持 SSR、Mini Program 或自定义平台。

⚠️ 潜在问题:

  • 模块间依赖紧密,若扩展不当可能破坏 AST 一致性;
  • 插件开发时需谨慎操作节点结构,否则易导致编译错误;
  • SourceMap 支持仍依赖外部工具,可能影响调试精度。

结语
Vue 编译器的模块化导出设计体现了其高度的可扩展性与可维护性。从解析到生成的完整链路,既保持了灵活的插件机制,又确保了类型安全与错误可追踪性。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

Vue 编译器源码解析:错误系统(errors.ts)

作者 excel
2025年11月4日 18:29

本文将深入分析 Vue 编译器的错误定义与生成机制,涵盖其类型设计、错误枚举、错误构造逻辑开发模式下的诊断行为。源码路径通常为 packages/compiler-core/src/errors.ts


一、概念层:错误系统的角色

在 Vue 编译器中,错误模块的作用是:

  1. 定义统一的错误类型(CompilerError / CoreCompilerError)。
  2. 提供标准化的错误创建与抛出函数。
  3. 管理编译过程中的错误代码(ErrorCodes)与对应人类可读的信息。
  4. 在开发模式与生产模式之间提供差异化的错误输出。

Vue 编译器运行过程中(如模板解析、AST 转换、代码生成阶段),都会调用这些错误定义,以确保统一格式与调试体验。


二、原理层:类型与错误模型设计

1. CompilerError 接口

export interface CompilerError extends SyntaxError {
  code: number | string
  loc?: SourceLocation
}

逐行解释:

  • extends SyntaxError:Vue 的错误基础继承自 JS 原生 SyntaxError,保持语法错误语义一致。
  • code: 错误编号,用于快速定位错误类型。
  • loc: 源码位置信息(如行列号),便于在模板中精确定位错误。

2. CoreCompilerError 与泛型推断

export interface CoreCompilerError extends CompilerError {
  code: ErrorCodes
}

type InferCompilerError<T> = T extends ErrorCodes
  ? CoreCompilerError
  : CompilerError

设计逻辑:

  • CoreCompilerError 约束 code 类型为 ErrorCodes(强类型化枚举)。

  • InferCompilerError<T> 用于自动推断返回类型。

    • code 属于 ErrorCodes 枚举,则返回更严格的 CoreCompilerError
    • 否则返回一般性的 CompilerError

➡️ 作用:保证不同阶段生成的错误在类型系统中能自动分化,既灵活又安全。


三、对比层:错误处理策略

1. 默认错误与警告函数

export function defaultOnError(error: CompilerError): never {
  throw error
}

export function defaultOnWarn(msg: CompilerError): void {
  __DEV__ && console.warn(`[Vue warn] ${msg.message}`)
}

对比分析:

功能 defaultOnError defaultOnWarn
触发行为 抛出异常 控制台警告(仅开发环境)
返回类型 never(中断执行) void(不中断)
用途 解析或编译失败 非致命警告提示

➡️ 区别本质:编译器区分“致命错误(error)”与“非致命问题(warn)”。


四、实践层:错误创建逻辑

1. 错误工厂函数

export function createCompilerError<T extends number>(
  code: T,
  loc?: SourceLocation,
  messages?: { [code: number]: string },
  additionalMessage?: string,
): InferCompilerError<T> {
  const msg =
    __DEV__ || !__BROWSER__
      ? (messages || errorMessages)[code] + (additionalMessage || ``)
      : `https://vuejs.org/error-reference/#compiler-${code}`
  const error = new SyntaxError(String(msg)) as InferCompilerError<T>
  error.code = code
  error.loc = loc
  return error
}

逐行解析:

  1. 泛型 T extends number:限定错误码为数字(与枚举兼容)。

  2. messages 参数:允许外部传入自定义错误信息表。

  3. msg 构建逻辑

    • 在开发环境或 Node 环境中,输出可读信息;
    • 在浏览器生产环境中,仅提供错误文档链接。
      👉 减少打包体积,提高线上安全性。
  4. 错误对象封装

    • 创建 SyntaxError 实例;
    • 动态附加 codeloc
    • 返回强类型化错误。

示例:

throw createCompilerError(ErrorCodes.X_V_IF_NO_EXPRESSION, loc)

➡️ 输出错误信息:“v-if/v-else-if is missing expression.”


五、拓展层:错误码体系与消息表

1. 错误码枚举 ErrorCodes

export enum ErrorCodes {
  // parse errors
  ABRUPT_CLOSING_OF_EMPTY_COMMENT,
  CDATA_IN_HTML_CONTENT,
  DUPLICATE_ATTRIBUTE,
  ...
  // transform errors
  X_V_IF_NO_EXPRESSION,
  X_V_SLOT_DUPLICATE_SLOT_NAMES,
  ...
  // generic errors
  X_PREFIX_ID_NOT_SUPPORTED,
  X_MODULE_MODE_NOT_SUPPORTED,
  ...
  __EXTEND_POINT__,
}

设计原则:

  • 分层管理

    • parse errors:模板解析错误;
    • transform errors:AST 转换错误;
    • generic errors:运行配置相关错误。
  • 顺序保留:注释指出 __EXTEND_POINT__ 位置用于扩展,避免未来版本冲突。


2. 错误信息表 errorMessages

export const errorMessages: Record<ErrorCodes, string> = {
  [ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT]: 'Illegal comment.',
  [ErrorCodes.DUPLICATE_ATTRIBUTE]: 'Duplicate attribute.',
  ...
  [ErrorCodes.X_V_IF_NO_EXPRESSION]: `v-if/v-else-if is missing expression.`,
  ...
}

要点说明:

  • 以枚举成员为键,保持一一映射;
  • 字符串内容简洁直接;
  • 特殊符号使用转义或 Unicode 编码(避免 HTML 解析冲突);
  • Vue 特有错误如 X_V_MODEL_ON_PROPS 带有详细指导说明。

示例说明:

ErrorCodes.X_V_MODEL_ON_PROPS =>
"v-model cannot be used on a prop, because local prop bindings are not writable..."

➡️ 该设计使 Vue 模板编译错误能准确指向开发者可修复的问题。


六、潜在问题与优化方向

  1. 错误码与消息耦合度高

    • 若新增错误需同时修改枚举与映射表,存在维护同步成本。
      ✅ 可通过代码生成或脚本验证提升一致性。
  2. 国际化支持缺失

    • 当前仅提供英文信息;未来可扩展多语言错误消息文件。
  3. 运行时与编译时混淆风险

    • ErrorCodes 仅作用于编译阶段,但有时开发者误认为适用于运行期错误捕获。

七、总结与启示

Vue 编译器错误模块的设计体现了三大原则:

  1. 类型安全:通过泛型与枚举保证错误结构统一。
  2. 调试友好:开发环境中输出完整信息,生产环境中输出文档链接。
  3. 可扩展性:使用 __EXTEND_POINT__ 预留错误码空间。

这种设计方式非常适合大型前端编译器、DSL(领域特定语言)或代码生成工具使用。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

Vue 编译器核心:baseCompile 源码深度解析

作者 excel
2025年11月4日 18:28

一、背景与概念

Vue 的编译器(compiler)负责将模板字符串(template)转换为可执行的渲染函数(render function)。
这个过程大致分为三个阶段:

  1. 解析(Parse) :将模板字符串解析为抽象语法树(AST)。
  2. 转换(Transform) :遍历并改造 AST 节点,应用各种指令转换(如 v-ifv-for)。
  3. 代码生成(Codegen) :将最终的 AST 生成 JavaScript 渲染函数代码。

baseCompile 就是这一整个编译流程的核心函数。它被更高层的模块(如 @vue/compiler-dom)包装,用于浏览器或 SSR 场景。


二、核心函数结构与执行流程

1. 文件导入部分

import type { CompilerOptions } from './options'
import { baseParse } from './parser'
import {
  type DirectiveTransform,
  type NodeTransform,
  transform,
} from './transform'
import { type CodegenResult, generate } from './codegen'
import type { RootNode } from './ast'
import { extend, isString } from '@vue/shared'

解释:

  • baseParse:负责将模板解析为 AST。
  • transform:负责节点遍历和转换。
  • generate:根据最终的 AST 输出渲染函数代码。
  • extend:对象合并工具函数(浅拷贝,用于合并编译选项)。

2. 转换预设 getBaseTransformPreset

export type TransformPreset = [
  NodeTransform[],
  Record<string, DirectiveTransform>,
]

export function getBaseTransformPreset(
  prefixIdentifiers?: boolean,
): TransformPreset {
  return [
    [
      transformVBindShorthand,
      transformOnce,
      transformIf,
      transformMemo,
      transformFor,
      ...(__COMPAT__ ? [transformFilter] : []),
      ...(!__BROWSER__ && prefixIdentifiers
        ? [
            trackVForSlotScopes,
            transformExpression,
          ]
        : __BROWSER__ && __DEV__
          ? [transformExpression]
          : []),
      transformSlotOutlet,
      transformElement,
      trackSlotScopes,
      transformText,
    ],
    {
      on: transformOn,
      bind: transformBind,
      model: transformModel,
    },
  ]
}

解释:

  • 该函数定义了 默认转换插件集(preset),包括:

    • 节点级转换(NodeTransform) :对整个节点结构生效的转换器。
    • 指令级转换(DirectiveTransform) :只针对特定指令(如 v-onv-bind)。
  • prefixIdentifiers 参数用于控制是否在编译阶段前缀变量(如 SSR、module 模式时)。

主要节点转换:

转换器 功能
transformIf 处理 v-if 逻辑结构
transformFor 处理 v-for 列表渲染
transformExpression 将表达式中的变量转化为作用域内标识符
transformElement 处理普通元素节点
transformText 合并相邻文本节点、插值等
transformModel 处理 v-model 指令
transformOn 处理 v-on 事件绑定
transformBind 处理 v-bind 动态属性

3. 编译主函数 baseCompile

export function baseCompile(
  source: string | RootNode,
  options: CompilerOptions = {},
): CodegenResult {
  const onError = options.onError || defaultOnError
  const isModuleMode = options.mode === 'module'

解释:

  • source:可以是模板字符串或已生成的 AST。
  • options:编译配置项。
  • onError:错误处理回调。
  • isModuleMode:是否为模块模式(通常用于 SSR)。

4. 浏览器/模块模式检查

  if (__BROWSER__) {
    if (options.prefixIdentifiers === true) {
      onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
    } else if (isModuleMode) {
      onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
    }
  }

解释:

  • 浏览器模式下不支持 prefixIdentifiersmodule mode
  • 在浏览器中,模板直接在运行时编译,无需模块作用域隔离。

5. 前缀与缓存处理逻辑

  const prefixIdentifiers =
    !__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
  if (!prefixIdentifiers && options.cacheHandlers) {
    onError(createCompilerError(ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED))
  }
  if (options.scopeId && !isModuleMode) {
    onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
  }

解释:

  • prefixIdentifiers 只有在非浏览器环境才可启用。
  • 当未启用 prefixIdentifiers 时,如果还希望缓存事件处理函数(cacheHandlers),会报错。
  • scopeId 通常只用于 module 模式(例如 SSR 时生成作用域 CSS)。

6. 解析模板为 AST

  const resolvedOptions = extend({}, options, {
    prefixIdentifiers,
  })
  const ast = isString(source) ? baseParse(source, resolvedOptions) : source

解释:

  • 将字符串模板解析为 AST。
  • source 已是 AST,则跳过解析阶段。

7. 获取转换预设与 TypeScript 支持

  const [nodeTransforms, directiveTransforms] =
    getBaseTransformPreset(prefixIdentifiers)

  if (!__BROWSER__ && options.isTS) {
    const { expressionPlugins } = options
    if (!expressionPlugins || !expressionPlugins.includes('typescript')) {
      options.expressionPlugins = [...(expressionPlugins || []), 'typescript']
    }
  }

解释:

  • 动态引入基础转换器。
  • 如果开启了 isTS 选项,则自动为表达式添加 TypeScript 插件支持(比如解析 TS 语法)。

8. 执行 AST 转换

  transform(
    ast,
    extend({}, resolvedOptions, {
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []),
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {},
      ),
    }),
  )

解释:

  • 执行核心转换阶段:

    • 将所有 nodeTransforms 顺序作用于 AST。
    • 同时允许用户通过 options 注入自定义转换器。
  • 例如,transformIf 会将:

    <div v-if="ok">yes</div>
    

    转换为条件表达式结构的渲染指令节点。


9. 代码生成阶段

  return generate(ast, resolvedOptions)
}

解释:

  • 最终调用 generate() 将优化后的 AST 转为 JavaScript 渲染函数。

  • 输出的 CodegenResult 通常包含:

    • code: 渲染函数的字符串源码。
    • ast: 最终 AST。
    • map: 源码映射(用于调试)。

三、原理总结

阶段 函数 功能
解析 baseParse 模板字符串 → AST
转换 transform 对 AST 应用转换规则
生成 generate AST → 渲染函数源码

Vue 的编译器是一个典型的 编译管线(pipeline)模式,每一步都在加工 AST,直到最终生成高性能的渲染函数。


四、与 DOM 编译器的对比

特性 @vue/compiler-core @vue/compiler-dom
环境 通用基础层 浏览器专用
输出 平台无关的渲染函数 带 DOM API 的渲染函数
转换器 v-ifv-for 等核心指令 DOM 事件、属性、样式相关
角色 被上层编译器继承和扩展 实际对模板进行编译输出

五、实践示例

输入模板:

<div v-if="ok">{{ msg }}</div>

执行:

const { code } = baseCompile('<div v-if="ok">{{ msg }}</div>')
console.log(code)

输出(简化版):

return function render(_ctx, _cache) {
  return _ctx.ok
    ? (_openBlock(), _createElementBlock("div", null, _toDisplayString(_ctx.msg), 1))
    : _createCommentVNode("v-if", true)
}

说明:

  • transformIfv-if 转为条件表达式结构;
  • transformText 处理 {{ msg }}
  • generate 输出完整渲染函数。

六、拓展与潜在问题

拓展方向

  • 用户可自定义 nodeTransforms 来实现自己的编译优化(如 AST 静态提升)。
  • 可结合 TypeScript 插件系统支持更复杂的表达式解析。

潜在问题

  • 编译选项之间存在依赖约束(如 prefixIdentifierscacheHandlers 不能同时用)。
  • 过多自定义转换器可能影响编译性能。
  • 浏览器模式下的错误检查有限,需在构建时编译模板。

七、总结

baseCompile 是 Vue 编译系统的“心脏”,其职责是将模板转化为高效的渲染函数。
整个流程体现了函数式编译管线思想:解析 → 转换 → 生成,每一步都职责清晰、可扩展性强。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

Vue 3 编译器源码深度解析:codegen.ts 模块详解

作者 excel
2025年11月4日 18:25

一、概念:Codegen 在 Vue 编译流程中的地位

在 Vue 3 的模板编译器(@vue/compiler-core)中,编译流程分为三个阶段:

  1. Parse(解析) :把模板字符串转为 AST(抽象语法树)。
  2. Transform(转换) :遍历 AST,标记指令、组件、静态节点等信息。
  3. Codegen(代码生成) :将优化过的 AST 转化为可执行的渲染函数源码(即 render)。

codegen.ts 模块的核心职责就是完成第 3 步:

把抽象语法树(AST)转化为 JavaScript 渲染函数源码。

它生成的最终代码类似下面这样:

function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode: _createVNode, openBlock: _openBlock } = Vue
    return (_openBlock(), _createVNode("div", null, "hello world"))
  }
}

二、原理:Codegen 的上下文与核心函数设计

1. CodegenContext:代码生成的运行环境

createCodegenContext 函数中,Vue 创建了一个上下文对象,保存了代码生成的所有状态:

interface CodegenContext {
  code: string        // 生成的代码字符串
  line: number        // 当前行号
  column: number      // 当前列号
  indentLevel: number // 缩进层次
  push(code: string): void // 添加代码片段
  indent(): void      // 增加缩进
  deindent(): void    // 减少缩进
  newline(): void     // 添加换行
}

👉 设计思路
Codegen 的整个过程其实就是「字符串拼接 + 结构控制」的过程。Vue 通过 Context 对象封装了“代码生成的状态”,使得生成过程可控且可追踪。

2. 代码生成入口:generate(ast, options)

核心函数 generate() 接受一个 RootNode(即整个模板的 AST 根节点)和编译选项:

export function generate(ast: RootNode, options: CodegenOptions): CodegenResult

它的内部逻辑大致如下:

  1. 创建上下文 context
  2. 生成代码前导部分(genModulePreamblegenFunctionPreamble);
  3. 写入渲染函数签名;
  4. 调用 genNode(ast.codegenNode) 递归生成主体代码;
  5. 返回完整的 { code, map } 结果。

这与 Babel、Rollup 等代码生成器的理念一致:递归遍历 + 拼接输出


三、对比:Vue 2.x 的模板编译生成逻辑

Vue 2.x 的模板编译由 compileToFunctions() 完成,生成的代码是字符串拼接版的 with(this){ return _c(...) }

而 Vue 3.x 的新设计引入了:

  • 模块化 AST(NodeTypes)结构
  • Context 管理代码位置
  • SourceMap 支持(基于 source-map-js)
  • 更细粒度的 helper 引入机制(如 _createVNode, _openBlock 等)。
对比项 Vue 2.x Vue 3.x
模板解析结构 自定义 parser 明确的 AST 节点类型体系
渲染函数输出 字符串拼接 结构化 codegen 过程
SourceMap ✅ 支持
Helper 引入 全局 Vue 对象访问 按需导入辅助函数
目标代码 with(this) 风格 支持 module/function 双模式

四、实践:生成一个最简渲染函数

让我们看一个简单的例子,模板如下:

<div>{{ msg }}</div>

编译后的核心 AST(简化版)如下:

{
  "type": 0, // RootNode
  "children": [
    {
      "type": 1, // ELEMENT
      "tag": "div",
      "children": [
        {
          "type": 5, // INTERPOLATION
          "content": { "type": 4, "content": "msg" }
        }
      ]
    }
  ]
}

生成函数调用流程为:

generate(ast)
  ↓
createCodegenContext()
  ↓
genFunctionPreamble()
  ↓
genVNodeCall()
  ↓
push("return _createVNode('div', null, _toDisplayString(msg))")

最后输出的代码:

function render(_ctx, _cache) {
  with (_ctx) {
    const { toDisplayString: _toDisplayString, createVNode: _createVNode } = Vue
    return _createVNode("div", null, _toDisplayString(msg))
  }
}

逐步注释:

  • with (_ctx):把模板内变量都解析为 _ctx.xxx
  • _toDisplayString(msg):把插值表达式安全地转为字符串。
  • _createVNode("div", ...):创建虚拟节点对象。

五、拓展:SourceMap 与辅助函数生成

1. SourceMap 支持

codegen.ts 内部对 SourceMapGenerator 做了封装(CodegenSourceMapGenerator),
在生成每一段代码时,记录源模板中对应的位置(loc.startloc.end),
从而支持 IDE 中的「反查模板源」。

addMapping(node.loc.start, name)

这让 Vue 的编译结果能精准映射到模板行号,有助于调试和热重载。

2. 辅助函数导入

Vue 会根据 AST 中用到的功能,自动按需引入运行时 helper:

import { createVNode as _createVNode, toDisplayString as _toDisplayString } from "vue"

这些 helper 在 runtimeHelpers.ts 中定义,用于构建虚拟 DOM、表达式求值等。


六、潜在问题与性能考量

  1. 性能优化

    • Vue 在 codegen 阶段通过 hoistStatic() 提前提取静态节点;
    • PURE_ANNOTATION 标记配合 Terser 可实现静态折叠。
  2. 安全性问题

    • 由于生成代码使用 new Function() 执行,因此模板内容需先通过 parse 阶段安全过滤。
  3. 可读性权衡

    • 代码生成逻辑极度模块化(数百个 NodeType 分支),可维护性优异但初学门槛高。
  4. SourceMap 性能损耗

    • 对大型模板启用 sourceMap: true 时可能影响编译速度。
    • Vue 内部通过 _mappings.add() 直接写入以减轻性能开销。

七、结语

codegen.ts 是 Vue 编译器中最复杂、但也最具系统性的模块之一。
它将 AST 与运行时世界衔接起来,最终产出浏览器可执行的渲染逻辑。
通过这一层,你可以清楚理解从模板到 Virtual DOM 的“最后一跳”。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

Vue 编译器源码解读:transformVBindShorthand 的设计与原理

作者 excel
2025年11月4日 18:05

在 Vue 模板编译器中,transformVBindShorthand 是一个专门处理 v-bind 简写语法(即 :prop)的节点转换器。
它的功能看似简单,却体现了 Vue 编译器对模板语法一致性与安全性的严谨设计。


一、概念:v-bind 与同名简写

在 Vue 模板中,常见的几种写法如下:

<!-- 完整写法 -->
<div v-bind:foo="bar"></div>

<!-- 简写写法 -->
<div :foo="bar"></div>

<!-- 同名简写写法 -->
<div :foo></div>

其中,:foo 等价于 :foo="foo",也就是「同名简写」形式。
transformVBindShorthand 的职责就是在编译时,将这种「简写绑定」自动扩展为完整的表达式绑定。


二、源码结构与逻辑概览

import { camelize } from '@vue/shared'
import {
  NodeTypes,
  type SimpleExpressionNode,
  createSimpleExpression,
} from '../ast'
import type { NodeTransform } from '../transform'
import { ErrorCodes, createCompilerError } from '../errors'
import { validFirstIdentCharRE } from '../utils'

模块导入说明:

  1. camelize:用于将属性名转换为驼峰形式,如 foo-bar → fooBar
  2. NodeTypes:定义 AST 节点类型常量。
  3. createSimpleExpression:生成一个新的表达式节点。
  4. NodeTransform:节点转换函数类型定义。
  5. createCompilerError:用于抛出编译阶段错误。
  6. validFirstIdentCharRE:用于检测变量名的首字符是否合法。

三、主函数:transformVBindShorthand

export const transformVBindShorthand: NodeTransform = (node, context) => {
  if (node.type === NodeTypes.ELEMENT) {
    for (const prop of node.props) {
      // same-name shorthand - :arg is expanded to :arg="arg"
      if (
        prop.type === NodeTypes.DIRECTIVE &&
        prop.name === 'bind' &&
        !prop.exp
      ) {
        const arg = prop.arg!

逻辑说明:

  • 只处理元素节点(ELEMENT)。
  • 遍历每个属性(props)。
  • 检查是否是 v-bind 指令,且没有表达式(即 :foo 而非 :foo="bar")。

arg 是绑定的参数节点,比如 foo(来自 :foo)。


四、错误检测与创建表达式

if (arg.type !== NodeTypes.SIMPLE_EXPRESSION || !arg.isStatic) {
  // only simple expression is allowed for same-name shorthand
  context.onError(
    createCompilerError(
      ErrorCodes.X_V_BIND_INVALID_SAME_NAME_ARGUMENT,
      arg.loc,
    ),
  )
  prop.exp = createSimpleExpression('', true, arg.loc)
}

原理:

  • :foofoo 不是静态简单表达式(比如 :[foo]),则报错。
  • 因为简写形式 :foo 只允许静态标识符,不允许复杂表达式。
  • 发生错误时,生成一个空表达式,防止后续编译崩溃。

注释解析:

// 允许的情况::foo
// 不允许的情况::[foo]、:foo-bar、:foo.baz

五、合法处理与属性名规范化

else {
  const propName = camelize((arg as SimpleExpressionNode).content)
  if (
    validFirstIdentCharRE.test(propName[0]) ||
    // allow hyphen first char for https://github.com/vuejs/language-tools/pull/3424
    propName[0] === '-'
  ) {
    prop.exp = createSimpleExpression(propName, false, arg.loc)
  }
}

核心逻辑:

  1. 使用 camelize() 将属性名规范化为驼峰形式。

    • 例如 :foo-bar:fooBar="fooBar".
  2. 检查首字符是否为合法标识符。

    • validFirstIdentCharRE 通常匹配 [A-Za-z$_]
    • 特例:允许 - 开头(用于某些自定义语法兼容)。
  3. 若合法,则创建表达式节点 fooBar,并赋给 prop.exp

最终,该节点会在生成代码阶段被转译为:

{
  props: { fooBar: fooBar }
}

六、完整流程图

:foo  →  检测无表达式
     ↓
验证 arg 是否为静态表达式
     ↓
是 → camelize(arg)
     ↓
检查首字符是否合法
     ↓
合法 → 生成 prop.exp = "foo"

七、对比:与普通 v-bind 的差异

写法 编译输入 生成表达式 特点
:foo="bar" 含表达式 "bar" 常规绑定
:foo 无表达式 "foo" 同名简写
:[foo] 动态参数 报错 不允许复杂表达式

该设计保证了模板语法的简洁性与安全性:
即“简写只允许静态同名”,避免编译时的不确定行为。


八、实践与应用场景

示例:

<template>
  <input :value />
</template>

编译结果(伪代码):

createElementVNode("input", {
  value: value
})

编译器自动补全表达式 "value",简化开发者手动输入。


九、拓展与演进方向

Vue 团队在 PR #3424 中曾对简写语法进行调整,允许 - 开头的属性名,用于工具链的类型推导兼容性。
这一修改也体现了编译器在语言演进过程中的「语义弹性」。


十、潜在问题与注意事项

  1. 动态参数不支持: [foo] 会触发编译错误。
  2. 非标识符属性名:如 :123:@click 等都会报错。
  3. 过度依赖驼峰化camelize 转换后可能与原始属性不匹配,需注意框架内部规范。

总结

transformVBindShorthand 虽是 Vue 编译流程中的一个小模块,却精准体现了编译器设计哲学:
在语法层保持简洁,在语义层保持严谨,在错误层保持可恢复性。

通过对这一函数的解析,我们可以看到 Vue 如何将模板语法的“糖衣”平滑地映射为编译期的语义树结构。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

❌
❌