阅读视图

发现新文章,点击刷新页面。

Vue 编译核心中的运行时辅助函数注册机制详解

一、概念说明

在 Vue 3 的编译流程中,**runtimeHelpers(运行时辅助函数)**是一组编译器与运行时之间的“桥梁”。
编译器在将模板编译为渲染函数(render function)时,会将某些指令(如 v-modelv-onv-show)转换为运行时调用的辅助函数。而这些辅助函数的引用与注册关系,就由 registerRuntimeHelpers() 维护。

换句话说:编译器不会直接在代码中写 withModifiers(...),而是写一个内部符号(Symbol),再通过映射表告诉运行时该符号对应哪个实际函数。


二、源码与原理解析

import { registerRuntimeHelpers } from '@vue/compiler-core'

// 定义编译时使用的唯一符号(用于标识运行时 helper 函数)
export const V_MODEL_RADIO: unique symbol = Symbol(__DEV__ ? `vModelRadio` : ``)
export const V_MODEL_CHECKBOX: unique symbol = Symbol(
  __DEV__ ? `vModelCheckbox` : ``,
)
export const V_MODEL_TEXT: unique symbol = Symbol(__DEV__ ? `vModelText` : ``)
export const V_MODEL_SELECT: unique symbol = Symbol(
  __DEV__ ? `vModelSelect` : ``,
)
export const V_MODEL_DYNAMIC: unique symbol = Symbol(
  __DEV__ ? `vModelDynamic` : ``,
)

export const V_ON_WITH_MODIFIERS: unique symbol = Symbol(
  __DEV__ ? `vOnModifiersGuard` : ``,
)
export const V_ON_WITH_KEYS: unique symbol = Symbol(
  __DEV__ ? `vOnKeysGuard` : ``,
)

export const V_SHOW: unique symbol = Symbol(__DEV__ ? `vShow` : ``)

export const TRANSITION: unique symbol = Symbol(__DEV__ ? `Transition` : ``)
export const TRANSITION_GROUP: unique symbol = Symbol(
  __DEV__ ? `TransitionGroup` : ``,
)

// 注册这些符号与其对应的运行时函数名称的映射关系
registerRuntimeHelpers({
  [V_MODEL_RADIO]: `vModelRadio`,
  [V_MODEL_CHECKBOX]: `vModelCheckbox`,
  [V_MODEL_TEXT]: `vModelText`,
  [V_MODEL_SELECT]: `vModelSelect`,
  [V_MODEL_DYNAMIC]: `vModelDynamic`,
  [V_ON_WITH_MODIFIERS]: `withModifiers`,
  [V_ON_WITH_KEYS]: `withKeys`,
  [V_SHOW]: `vShow`,
  [TRANSITION]: `Transition`,
  [TRANSITION_GROUP]: `TransitionGroup`,
})

逐行注释说明:

  • Symbol(__DEV__ ? 'vModelRadio' : '')
    → 创建一个独立的唯一符号。
    在开发模式下(__DEV__true)使用可读字符串方便调试;
    在生产模式下为空字符串以减小体积。
  • unique symbol
    → TypeScript 类型系统中的特殊标识符,确保该常量唯一、不可重名。
  • registerRuntimeHelpers()
    → 将这些符号与对应的运行时函数名建立映射。
    编译器后续生成代码时,就可以通过符号查找到对应的 Helper。

三、机制对比分析

特性 Vue 2.x 实现 Vue 3 实现
辅助函数声明 直接字符串引用(如 _vModel 使用 Symbol 唯一标识
编译与运行时绑定 模糊绑定(通过命名约定) 显式映射(registerRuntimeHelpers
Tree-shaking 较弱 可按需引入、极强
类型安全 通过 unique symbol 强类型保证

👉 结论:Vue 3 通过 Symbol 注册机制,使得运行时函数调用更加安全、可追踪且利于优化。


四、实践示例:编译阶段的 Helper 替换

模板示例:

<input v-model="checked" type="checkbox" />

编译后伪代码:

// 编译器在生成 AST → 渲染函数的过程中
// 发现 v-model + type="checkbox" => 对应 helper 为 V_MODEL_CHECKBOX

import { V_MODEL_CHECKBOX } from './runtimeHelpers'

function render(_ctx) {
  return _createElementVNode("input", {
    type: "checkbox",
    "onUpdate:modelValue": _cache[0] || (_cache[0] = _withDirectives(...))
  }, null, 512 /* NEED_PATCH */)
}

在最终构建输出阶段,Vue 会根据注册的映射表:

[V_MODEL_CHECKBOX]: 'vModelCheckbox'

将辅助函数替换为真实的运行时代码:

import { vModelCheckbox } from 'vue'

五、拓展思考:为何使用 Symbol?

  1. 唯一性保证
    即使不同模块导入相同 helper,也不会冲突。
  2. 调试友好性
    在开发环境下,Symbol 描述字符串会在控制台中显示,方便分析。
  3. 可扩展性
    未来若新增指令(如自定义指令 helper),可直接定义新 Symbol 注册即可,不破坏原有逻辑。

六、潜在问题与注意事项

潜在问题 说明 解决建议
Symbol 在生产环境中无描述 可能导致调试信息缺失 保留 DEV 构建版本以调试
registerRuntimeHelpers 顺序不当 若多次注册重复 key,会覆盖前者 遵守统一注册顺序并集中管理
运行时未导出对应函数 导致渲染阶段报错 “helper not found” 确保 runtime-dom 中对应函数存在
Tree-shaking 失效 若错误导入全部 runtime 应仅按需引用 helper 模块

七、总结

Vue 编译核心中的 runtimeHelpers模板编译到运行时执行的关键枢纽
它通过:

  • 使用 Symbol 实现唯一标识;
  • 通过 registerRuntimeHelpers() 建立映射;
  • 将编译器生成的抽象指令转译为运行时真实函数。

这一机制实现了编译与运行的解耦、类型安全、可维护与高效优化


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

Vue 模板解析器 parserOptions 深度解析

一、概念概述

在 Vue 的编译流程中,模板解析(template parsing) 是编译器的第一步。
其任务是将用户编写的 HTML 模板字符串转换为抽象语法树(AST,Abstract Syntax Tree)。
这一过程由 @vue/compiler-core 提供核心逻辑,而各平台(浏览器、SSR、小程序等)可以通过自定义 parserOptions 来决定如何识别标签、命名空间和内建组件。

本文所展示的 parserOptions 即是 Vue 浏览器端编译器的解析配置


二、源码原理分析

import { Namespaces, NodeTypes, type ParserOptions } from '@vue/compiler-core'
import { isHTMLTag, isMathMLTag, isSVGTag, isVoidTag } from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { decodeHtmlBrowser } from './decodeHtmlBrowser'

export const parserOptions: ParserOptions = {
  parseMode: 'html',
  isVoidTag,
  isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag) || isMathMLTag(tag),
  isPreTag: tag => tag === 'pre',
  isIgnoreNewlineTag: tag => tag === 'pre' || tag === 'textarea',
  decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined,
  ...
}

(1) 模式与基础判断函数

  • parseMode: 'html'
    说明当前解析器的输入是 HTML 模板,而非 JSX 或其他 DSL。
  • isVoidTag
    判断一个标签是否是空标签(void element),如 <img>, <br>, <input> 等,这些标签不允许子节点。
  • isNativeTag
    通过 isHTMLTag / isSVGTag / isMathMLTag 判断标签是否为原生标签,防止自定义组件被误识别为 HTML。
  • isPreTagisIgnoreNewlineTag
    控制是否保留换行符。例如 <pre><textarea> 的内容应原样保留。
  • decodeEntities
    在浏览器环境中使用 decodeHtmlBrowser 进行 HTML 实体解码(如 &amp;&)。

(2) 内建组件识别逻辑

isBuiltInComponent: tag => {
  if (tag === 'Transition' || tag === 'transition') {
    return TRANSITION
  } else if (tag === 'TransitionGroup' || tag === 'transition-group') {
    return TRANSITION_GROUP
  }
},

说明:

Vue 中有两个特殊的内建组件:

  • <Transition>:单元素/组件的过渡动画;
  • <TransitionGroup>:多个元素的列表动画。

这里的函数返回 运行时标识符(runtime helper) ,由编译器注入至渲染函数中,用以连接模板编译结果与运行时逻辑。

💡 关键点
在编译阶段,Vue 会将 <Transition> 转换为一个特殊的 AST 节点,并通过 TRANSITION 常量关联到运行时的过渡逻辑。


(3) 命名空间解析逻辑 getNamespace

getNamespace(tag, parent, rootNamespace) {
  let ns = parent ? parent.ns : rootNamespace
  if (parent && ns === Namespaces.MATH_ML) {
    ...
  } else if (parent && ns === Namespaces.SVG) {
    ...
  }

  if (ns === Namespaces.HTML) {
    if (tag === 'svg') {
      return Namespaces.SVG
    }
    if (tag === 'math') {
      return Namespaces.MATH_ML
    }
  }
  return ns
},

功能说明:

Vue 解析模板时,会为每个节点维护一个命名空间:

  • Namespaces.HTML
  • Namespaces.SVG
  • Namespaces.MATH_ML

这些命名空间控制编译器如何处理节点与属性,例如:

  • SVG 元素的属性名区分大小写;
  • MathML 中的结构与 HTML 不同。

详细逻辑:

  1. 从父节点继承命名空间
    默认继承父节点的 ns

  2. 特殊处理 MathML

    • 如果父节点是 <annotation-xml> 且包含 encoding="text/html"encoding="application/xhtml+xml",则切换到 HTML 命名空间;
    • 若父节点为 mtextmimo 等数学标签,且当前标签非 mglyphmalignmark,也切换到 HTML 命名空间(因为这类内容可含普通 HTML)。
  3. 特殊处理 SVG

    • 当父节点是 <foreignObject><desc><title> 时,其内部内容属于 HTML 语义。
  4. HTML → 子节点切换

    • <svg> → 切入 SVG 命名空间;
    • <math> → 切入 MathML 命名空间。

三、机制对比:HTML / SVG / MathML 解析差异

特性 HTML SVG MathML
命名空间 默认 http://www.w3.org/2000/svg http://www.w3.org/1998/Math/MathML
属性区分大小写
标签嵌套规则 自由 严格 严格
空标签规则 存在 void 元素 无 void 概念 无 void 概念

Vue 的 getNamespace 正是为了解决这些语法差异,使模板在不同命名空间中被正确解析。


四、实践:在自定义编译器中使用

你可以基于这个配置创建一个 自定义 HTML 编译器

import { baseCompile } from '@vue/compiler-core'
import { parserOptions } from './parserOptions'

const template = `<svg><foreignObject><div>Hello</div></foreignObject></svg>`
const ast = baseCompile(template, { parserOptions }).ast

console.log(ast)

输出结果(简化版):

{
  type: 1,
  tag: 'svg',
  ns: Namespaces.SVG,
  children: [
    {
      tag: 'foreignObject',
      ns: Namespaces.SVG,
      children: [
        { tag: 'div', ns: Namespaces.HTML }
      ]
    }
  ]
}

说明:

  • <svg> → SVG 命名空间;
  • <foreignObject> → 仍属于 SVG;
  • <div> → 切换回 HTML 命名空间(由 getNamespace 控制)。

五、拓展与衍生思考

  1. 在 SSR 场景下

    • decodeEntities 可能需要服务端版本,如 decodeHtml(非 decodeHtmlBrowser)。
  2. 在小程序编译中

    • 可替换 isNativeTag 判断逻辑,以识别小程序原生组件(如 viewbutton 等)。
  3. 在 JSX 模式中

    • parseMode 可设为 'jsx',并采用不同的节点构建逻辑。

六、潜在问题与优化方向

  • 问题 1:性能开销
    getNamespace 在嵌套结构复杂时频繁调用,理论上可缓存部分计算结果。
  • 问题 2:跨平台一致性
    不同运行时环境(Web、Weex、Custom Renderer)需保证 parserOptions 的一致性,否则 AST 结构不兼容。
  • 问题 3:命名空间边界模糊
    部分浏览器行为(如 <math><svg> 混用)存在兼容性差异,Vue 的命名空间策略是权衡后的实现。

七、结语

本文深入解析了 Vue 编译器中 parserOptions 的源码设计与实现逻辑,从标签判断到命名空间规则,再到内建组件映射,展示了 Vue 编译系统在“语法一致性”与“平台兼容性”之间的平衡思路。

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

🌿 深度解析 Vue DOM 编译器模块源码:compile 与 parse 的构建逻辑

一、概念层:Vue DOM 编译器的职责

Vue 的编译器(@vue/compiler-dom)是模板到渲染函数的桥梁。
其核心职责是:

  • 解析模板字符串 → 生成抽象语法树(AST)。
  • 执行节点和指令的转换 → 将模板语法映射为虚拟节点创建代码。
  • 代码生成 → 输出可执行的渲染函数。

本篇代码展示了 Vue DOM 编译器的入口实现逻辑,即 compileparse 的封装。


二、原理层:核心结构概览

import {
  baseCompile,
  baseParse,
  type CodegenResult,
  type CompilerOptions,
  type RootNode,
  type DirectiveTransform,
  type NodeTransform,
  noopDirectiveTransform,
} from '@vue/compiler-core'

🔍 解析说明

  • baseCompile / baseParse:来自 @vue/compiler-core,是核心的模板编译与解析引擎。
  • CompilerOptions:编译时配置项(自定义解析器、指令、节点转换等)。
  • noopDirectiveTransform:空指令转换器,用于如 v-cloak 这种无需编译逻辑的指令。

核心思想@vue/compiler-dom 是在 compiler-core 的基础上扩展 DOM 特有语法的一个“包装层”。


三、对比层:DOM 专属的增强点

compiler-dom 相较于 compiler-core,在下列方面有所增强:

模块 功能增强 说明
parserOptions 增加 HTML 语义解析规则 适应浏览器 DOM 特性
transformStyle 处理内联样式 将 style 属性转为动态绑定表达式
transformVHtml / transformVText 处理 v-htmlv-text 注入对应渲染逻辑
transformModel 重写 v-model 实现 DOM 层双向绑定
transformOn 重写 v-on 添加事件代理与修饰符逻辑
transformShow 实现 v-show 指令 控制元素显示隐藏
stringifyStatic 静态节点字符串化 提升 SSR 渲染性能

四、实践层:源码逐步拆解

1️⃣ 组合节点与指令的转换器

export const DOMNodeTransforms: NodeTransform[] = [
  transformStyle,
  ...(__DEV__ ? [transformTransition, validateHtmlNesting] : []),
]

🧩 注释与解释:

  • transformStyle:基础样式节点转换。
  • transformTransition:开发环境下检测 <transition> 组件。
  • validateHtmlNesting:在开发环境中校验 HTML 标签嵌套合法性。

⚙️ 在生产环境中,这两个校验逻辑将被剔除,以优化编译性能。


2️⃣ 注册 DOM 指令转换器

export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {
  cloak: noopDirectiveTransform,
  html: transformVHtml,
  text: transformVText,
  model: transformModel,
  on: transformOn,
  show: transformShow,
}

💡 注释与解释:

  • v-cloak → 无需生成代码,直接忽略。
  • v-html / v-text → 分别生成 innerHTMLtextContent 赋值逻辑。
  • v-model / v-on → 重写核心指令,兼容浏览器事件系统。
  • v-show → 转换为动态显示控制代码。

📌 此处通过覆盖同名指令实现“DOM 定制版”的行为。


3️⃣ 编译器入口:compile

export function compile(
  src: string | RootNode,
  options: CompilerOptions = {},
): CodegenResult {
  return baseCompile(
    src,
    extend({}, parserOptions, options, {
      nodeTransforms: [
        ignoreSideEffectTags,
        ...DOMNodeTransforms,
        ...(options.nodeTransforms || []),
      ],
      directiveTransforms: extend(
        {},
        DOMDirectiveTransforms,
        options.directiveTransforms || {},
      ),
      transformHoist: __BROWSER__ ? null : stringifyStatic,
    }),
  )
}

🧱 拆解讲解:

步骤 功能 说明
extend({}, parserOptions, options, …) 合并配置 保留用户自定义 transform
ignoreSideEffectTags 忽略副作用标签 跳过 <script> / <style>
nodeTransforms 节点转换序列 统一挂载 DOM 相关 transform
directiveTransforms 指令转换序列 扩展 DOM 特有指令
transformHoist 提升优化控制 在 SSR 模式下启用静态节点字符串化

✳️ 实践逻辑:

compile 的本质是对 baseCompile 的再包装。它利用 extendcompiler-core 的能力“注入” DOM 规则,实现:

模板 → AST → DOM 转换规则注入 → 渲染函数代码。


4️⃣ 模板解析:parse

export function parse(template: string, options: ParserOptions = {}): RootNode {
  return baseParse(template, extend({}, parserOptions, options))
}

🧩 说明:

  • baseParse 执行基础的模板词法与语法分析。
  • parserOptions 负责 DOM 标签规则与属性判断。

📜 返回值:AST 根节点 RootNode,包含所有模板结构信息。


五、拓展层:编译生态与复用机制

Vue 编译器实际上由多个包协同完成:

模块 作用 依赖关系
@vue/compiler-core 通用 AST & 渲染生成逻辑 被所有编译器复用
@vue/compiler-dom 针对浏览器 DOM 的实现 依赖 core
@vue/compiler-ssr 服务端渲染优化版 共享 transform 列表
@vue/compiler-sfc 单文件组件(.vue)处理 调用 DOM 编译器

因此,compiler-dom 是整个 Vue 编译体系的“前端模板层”。


六、潜在问题与优化思考

问题点 说明 潜在优化方向
环境分支(__DEV__ / __BROWSER__ 不同构建目标逻辑差异大 可考虑使用动态注入插件简化
transform 体系耦合度高 各 transform 需严格顺序执行 未来可通过 pipeline 化机制改进
静态提升与字符串化策略复杂 SSR 与 CSR 差异明显 可引入统一的中间层优化策略

七、总结

这份源码是 Vue 编译器 DOM 层的桥梁实现,核心目标是将 通用编译框架DOM 特定逻辑 解耦。
通过灵活的 transform 注册机制,它为浏览器端渲染提供了高扩展性的编译管线。


📘 结语
本文详细解析了 Vue compiler-dom 模块的设计逻辑与源码结构,从概念到实现层层剖析其构建思想。
掌握这一部分,可以更深入理解 Vue 编译器的工作机制及其生态模块间的分工。


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

深度解析:isValidHTMLNesting —— HTML 嵌套合法性验证的设计与实现

一、概念层:功能与设计目标

在前端编译器或模板编译阶段,我们经常需要判断一个 HTML 标签是否可以合法地嵌套在另一个标签内
例如:

  • <ul><li></li></ul> 是合法的。
  • <p><div></div></p> 是不合法的。

Vue 编译器的 DOM 转换阶段就需要这类判断,以在模板编译时抛出清晰的语法错误。
本模块正是为了解决这个问题——在不依赖外部库的前提下,提供 isValidHTMLNesting(parent, child) 方法,判断一对标签的父子关系是否合法。


二、原理层:逻辑流程与判断优先级

核心函数 isValidHTMLNesting(parent, child) 的逻辑可以概括为如下优先级顺序:

export function isValidHTMLNesting(parent: string, child: string): boolean {
  if (parent === 'template') return true; // 特例1:<template> 可包任何元素

  if (parent in onlyValidChildren)
    return onlyValidChildren[parent].has(child); // 特例2:父节点有明确允许子节点集合

  if (child in onlyValidParents)
    return onlyValidParents[child].has(parent); // 特例3:子节点有明确唯一父节点集合

  if (parent in knownInvalidChildren)
    if (knownInvalidChildren[parent].has(child)) return false; // 否定1:父节点禁止特定子节点

  if (child in knownInvalidParents)
    if (knownInvalidParents[child].has(parent)) return false; // 否定2:子节点禁止特定父节点

  return true; // 其他情况默认合法
}

逻辑顺序解析:

  1. 特例优先

    • <template> 这种结构性标签可包含任意元素。
    • <table><thead> 等标签对结构有严格约束,因此优先匹配 “onlyValidChildren”。
  2. 否定规则覆盖

    • 若某父节点明确定义了“不允许”的子节点(例如 <p> 内禁止 <div>),立即判定非法。
    • 同理,如果某子节点不能出现在某父节点中(例如 <a> 不能嵌套 <a>),也直接否决。
  3. 默认放行

    • 若不在规则集合中,则认为合法,以保持宽容性与未来兼容。

三、对比层:与 W3C / React / Vue 规则的异同

框架 嵌套验证策略 特点
W3C HTML Spec 规范性最强,定义复杂且细粒度 精确但实现困难
React DOM Validator 仅警告级别,不中断编译 偏向开发提示
Vue Compiler DOM 编译时校验并抛出错误 更严格、更前置
本实现 (validate-html-nesting) 静态映射规则 + 特例处理 性能高、零依赖、适合编译期使用

Vue 官方在 @vue/compiler-dom 中采用的就是这种轻量策略。
它不追求 100% 的 HTML 规范覆盖,而是确保绝大多数错误嵌套能在编译期被捕获


四、实践层:主要数据结构与源码解构

1. onlyValidChildren —— “父节点白名单”

const onlyValidChildren: Record<string, Set<string>> = {
  head: new Set(['base','link','meta','title','style','script','template']),
  select: new Set(['optgroup','option','hr']),
  table: new Set(['caption','colgroup','tbody','tfoot','thead']),
  tr: new Set(['td','th']),
  ...
  script: new Set([]), // script不可包含子元素
}

设计目的:
有些 HTML 标签有明确结构规则(如 <table> 只能含 <tr>)。
这些父节点拥有唯一合法子节点集合。

解析:

  • head → 仅允许 <meta><title><script> 等。
  • script / style 等则设置为空集合 emptySet,禁止任何子元素。

2. onlyValidParents —— “子节点白名单”

const onlyValidParents: Record<string, Set<string>> = {
  td: new Set(['tr']),
  tr: new Set(['tbody', 'thead', 'tfoot']),
  th: new Set(['tr']),
  figcaption: new Set(['figure']),
  summary: new Set(['details']),
  ...
}

作用:
部分元素只能出现在指定父节点中,如:

  • <td> 必须位于 <tr>
  • <tr> 只能位于 <tbody><thead>
  • <figcaption> 只能在 <figure> 中。

3. knownInvalidChildren —— “父节点黑名单”

const knownInvalidChildren: Record<string, Set<string>> = {
  p: new Set(['div','section','table','ul', ...]),
  svg: new Set(['div','span','p','table', ...]),
}

语义说明:

  • <p> 是行内块级元素,不能直接包含块级元素;
  • <svg> 是独立命名空间,不应包含普通 HTML 标签。

4. knownInvalidParents —— “子节点黑名单”

const knownInvalidParents: Record<string, Set<string>> = {
  a: new Set(['a']),
  button: new Set(['button']),
  form: new Set(['form']),
  li: new Set(['li']),
  h1: headings,
  ...
}

意义:

  • <a> 不能嵌套 <a>
  • <button> 不能嵌套 <button>
  • 标题标签 <h1>~<h6> 不应互相嵌套。

五、拓展层:改进方向与应用场景

1. 改进方向

  • 命名空间支持:目前未处理 SVG/MathML 的复杂子层级。
  • 动态规则加载:可从外部 JSON 自动同步更新。
  • 编译器集成:在 Vue 模板 AST 分析阶段可直接调用,辅助报错。

2. 实际应用场景

  • Vue 编译器插件:在 transformElement 阶段校验嵌套。
  • HTML 静态分析工具:用于 CI 语法检查。
  • 模板语言解析器(如 Pug/Handlebars) :转换前验证嵌套结构。

六、潜在问题与注意事项

问题 说明
规则更新延迟 原始仓库 validate-html-nesting 更新时需手动同步
非标准标签支持有限 自定义组件或 Web Components 默认视为合法
错误上下文缺失 函数仅返回 true/false,不提供错误原因或修复建议

七、结语

isValidHTMLNesting 是一个轻量但关键的 HTML 校验模块,
它的设计哲学是——在不引入运行时依赖的前提下,静态定义最主要的合法性规则
这使它非常适合前端编译阶段或静态分析工具使用。

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

Vue DOM 编译错误系统解析:DOMErrorCodes 与 createDOMCompilerError

一、概念

在 Vue 3 的模板编译过程中,错误系统(Error System) 用于在编译模板为渲染函数时检测和报告各种潜在问题。
本文中的代码片段来自 @vue/compiler-dom 模块,主要定义了 DOM 层级特有的错误码错误信息映射、以及用于创建错误对象的 createDOMCompilerError 方法。
这些错误是针对浏览器环境(如 v-htmlv-textv-model)等指令使用不当时产生的专属错误。


二、原理解析

1. 错误对象结构

export interface DOMCompilerError extends CompilerError {
  code: DOMErrorCodes
}

解释:

  • DOMCompilerError 继承自核心编译器的 CompilerError
  • 其核心属性是 code,标识具体错误类型(枚举值)。

这保证了 DOM 模块的错误能与核心编译器保持统一结构,同时又能拓展自定义错误。


2. 错误创建函数

export function createDOMCompilerError(
  code: DOMErrorCodes,
  loc?: SourceLocation,
) {
  return createCompilerError(
    code,
    loc,
    __DEV__ || !__BROWSER__ ? DOMErrorMessages : undefined,
  ) as DOMCompilerError
}

逐步说明:

  1. 参数

    • code: 来自 DOMErrorCodes 的错误编号。
    • loc: 可选的源码位置信息,用于定位错误在模板中的位置。
  2. 逻辑

    • 调用核心函数 createCompilerError() 生成错误对象。
    • 在开发模式(__DEV__)或非浏览器环境中,传入 DOMErrorMessages 以附带可读信息。
  3. 返回类型

    • 返回强制转换为 DOMCompilerError,保证类型安全。

设计思想:
编译器错误系统通过环境变量动态切换错误描述:

  • 生产环境 → 只保留错误码(节省体积);
  • 开发环境 → 附带人类可读的错误提示信息。

3. DOMErrorCodes 枚举定义

export enum DOMErrorCodes {
  X_V_HTML_NO_EXPRESSION = 53,
  X_V_HTML_WITH_CHILDREN,
  X_V_TEXT_NO_EXPRESSION,
  X_V_TEXT_WITH_CHILDREN,
  X_V_MODEL_ON_INVALID_ELEMENT,
  X_V_MODEL_ARG_ON_ELEMENT,
  X_V_MODEL_ON_FILE_INPUT_ELEMENT,
  X_V_MODEL_UNNECESSARY_VALUE,
  X_V_SHOW_NO_EXPRESSION,
  X_TRANSITION_INVALID_CHILDREN,
  X_IGNORED_SIDE_EFFECT_TAG,
  __EXTEND_POINT__,
}

逐项解读:

  • X_V_HTML_NO_EXPRESSIONv-html 缺少表达式。
  • X_V_HTML_WITH_CHILDRENv-html 使用时仍存在子节点(将被覆盖)。
  • X_V_TEXT_NO_EXPRESSIONv-text 缺少表达式。
  • X_V_MODEL_ON_INVALID_ELEMENTv-model 用在非表单元素上。
  • X_V_MODEL_ON_FILE_INPUT_ELEMENTv-model 不能绑定文件输入框。
  • X_TRANSITION_INVALID_CHILDREN<Transition> 内子节点数不正确。
  • X_IGNORED_SIDE_EFFECT_TAG<script> / <style> 等副作用标签被忽略。

关键点:
枚举起始值 53 来自 ErrorCodes.__EXTEND_POINT__,保证与 @vue/compiler-core 不冲突。
每个错误码都是自增生成的唯一整数值。


4. 枚举同步检查机制

if (__TEST__) {
  if (DOMErrorCodes.X_V_HTML_NO_EXPRESSION < ErrorCodes.__EXTEND_POINT__) {
    throw new Error(
      `DOMErrorCodes need to be updated to ${ErrorCodes.__EXTEND_POINT__}...`
    )
  }
}

功能:

  • 在单元测试模式下(__TEST__),确保 DOM 错误码起始位置不与核心错误码冲突。
  • 若版本不同步,则自动抛出异常提醒开发者更新常量。

⚙️ 设计亮点:
此校验机制确保了编译器多模块协作时的错误码空间隔离,防止编号重叠导致错误信息错乱。


5. 错误信息字典

export const DOMErrorMessages: { [code: number]: string } = {
  [DOMErrorCodes.X_V_HTML_NO_EXPRESSION]: `v-html is missing expression.`,
  [DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT]: 
    `v-model cannot be used on file inputs since they are read-only...`,
  ...
}

说明:
这是一个从错误码到提示语的映射表。
编译器在抛错时可以通过错误码查表,获得直观的提示文字。

典型输出(开发模式):

[Vue compiler error] v-model cannot be used on file inputs since they are read-only.

三、对比分析:与核心 ErrorCodes 的关系

模块 错误码前缀 作用域 典型错误
@vue/compiler-core ErrorCodes 通用模板语法 缺少表达式、无效指令
@vue/compiler-dom DOMErrorCodes 浏览器专用 v-html / v-model 错误

总结:
DOMErrorCodes 是对核心错误系统的扩展层,负责浏览器特定的语义验证。
它通过 __EXTEND_POINT__ 与核心模块形成一种“版本对齐机制”。


四、实践示例

假设我们在模板中误用了 v-model

<div v-model="data"></div>

在编译阶段将触发:

createDOMCompilerError(DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT, loc)

输出(开发环境):

[v-model can only be used on <input>, <textarea> and <select> elements.]

过程说明:

  1. 编译器检测到 div 上绑定 v-model
  2. 触发相应错误码;
  3. createDOMCompilerError 构造错误;
  4. 编译器捕获并输出至控制台。

五、拓展与思考

  • 扩展性设计
    __EXTEND_POINT__ 机制使未来可以安全添加新错误类型而不冲突。
  • 环境感知机制
    借助 __DEV____BROWSER__,Vue 能在不同构建目标下动态切换错误输出粒度。
  • 可测试性
    单元测试下自动检测错误码重叠,强化工程一致性。
  • 国际化潜力
    未来可在 DOMErrorMessages 上层封装语言包系统以支持多语言错误提示。

六、潜在问题与优化空间

  1. 手动同步风险
    若核心库 ErrorCodes.__EXTEND_POINT__ 更新但 DOM 模块未同步,测试才会检测到,属于事后发现型问题
  2. 错误信息冗余
    大量硬编码的错误字符串可能在不同语言版本中造成维护负担。
  3. 缺乏上下文恢复机制
    仅报告错误而不提供“修复建议”或 AST 位置恢复,可能影响 IDE 集成体验。

七、总结

Vue 的 DOMErrorCodescreateDOMCompilerError 模块展示了其编译系统的模块化设计哲学
通过清晰的错误码空间划分、环境自适应输出、以及开发测试保护机制,实现了高可维护性与可扩展性。
这套机制为 Vue 的模板编译器在不同运行时环境下提供了稳定、清晰的错误反馈体系。


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

深度解析:decodeHtmlBrowser —— 浏览器端 HTML 解码函数设计

一、背景与概念

在前端开发中,我们经常需要处理HTML 实体(HTML Entities) 。例如服务器返回的内容可能包含:

&lt;div&gt;Hello&lt;/div&gt;

这时前端需要将这些转义符还原成真实字符 <div>Hello</div>,以便正确展示或处理。
为此,浏览器内置的 DOMParser 或元素解析能力就能帮助我们实现HTML 解码

decodeHtmlBrowser 就是一个利用浏览器 DOM 的小型解码工具函数,它可以在浏览器端安全、高效地将被转义的 HTML 还原。


二、源码与逐行解析

/* eslint-disable no-restricted-globals */

let decoder: HTMLDivElement

export function decodeHtmlBrowser(raw: string, asAttr = false): string {
  if (!decoder) {
    decoder = document.createElement('div')
  }
  if (asAttr) {
    decoder.innerHTML = `<div foo="${raw.replace(/"/g, '&quot;')}">`
    return decoder.children[0].getAttribute('foo')!
  } else {
    decoder.innerHTML = raw
    return decoder.textContent!
  }
}

🔹 第 1 行:/* eslint-disable no-restricted-globals */

关闭 ESLint 规则 no-restricted-globals
该规则通常用于防止全局变量污染(如 eventnameself 等),此处禁用是为了确保使用 document 不被警告。


🔹 第 2 行:let decoder: HTMLDivElement

定义一个全局缓存变量,用于保存一个 <div> 元素。
作用:避免每次调用函数都重新创建 DOM 节点,提高性能。


🔹 第 4 行:函数定义

export function decodeHtmlBrowser(raw: string, asAttr = false): string
  • raw: 原始字符串(可能包含 HTML 实体)
  • asAttr: 是否以属性上下文解析,默认为 false

🔹 第 5–7 行:DOM 缓存初始化

if (!decoder) {
  decoder = document.createElement('div')
}

首次调用时创建一个 <div> 节点,之后多次复用。


🔹 第 8–11 行:属性模式(asAttr = true)

if (asAttr) {
  decoder.innerHTML = `<div foo="${raw.replace(/"/g, '&quot;')}">`
  return decoder.children[0].getAttribute('foo')!
}

解析逻辑:

  1. 先替换掉 ",防止破坏 HTML 结构。

    raw.replace(/"/g, '&quot;')
    
  2. 通过设置 innerHTML,让浏览器解析 HTML 实体。

  3. 再读取子节点第一个元素的 foo 属性,得到浏览器自动解码后的结果。

示例:

decodeHtmlBrowser('&lt;div&gt;x&lt;/div&gt;', true)
// => "<div>x</div>"

🔹 第 12–14 行:文本模式(asAttr = false)

decoder.innerHTML = raw
return decoder.textContent!

此模式下直接使用 <div>textContent 读取解码结果。

示例:

decodeHtmlBrowser('&amp;lt;Hello&amp;gt;')
// => "&lt;Hello&gt;"
decodeHtmlBrowser('&lt;Hello&gt;')
// => "<Hello>"

三、原理分析

模式 使用 DOM 属性 解码范围 典型场景
文本模式 textContent 通用 HTML 文本 用户输入、HTML 内容
属性模式 getAttribute 属性上下文转义 HTML 属性内的转义内容

本质上,浏览器的 DOM 解析器在解析 innerHTML 时会自动将实体符号转回字符,因此这段代码就是巧妙地利用浏览器的解析行为完成解码。


四、与其他方案对比

方法 原理 优点 缺点
decodeHtmlBrowser 利用 DOM 自动解析 兼容性好、无需外部库 需在浏览器环境
DOMParser 创建解析文档 更安全(不污染现有 DOM) 代码稍繁琐
he(npm 包) JS 实现 HTML 实体表 支持全实体 文件体积较大

✅ 实际开发中,如果只在浏览器端运行,该函数足够轻量且性能良好。


五、实践示例

示例 1:解码普通文本

decodeHtmlBrowser('&lt;span&gt;Hi&lt;/span&gt;')
// 输出:"<span>Hi</span>"

示例 2:解码属性值

decodeHtmlBrowser('Tom &amp; Jerry', true)
// 输出:"Tom & Jerry"

示例 3:性能优化

由于 decoder 是全局复用的,连续调用不会重复创建 DOM 节点,非常适合在循环中解码大量字符串。


六、拓展思考

可以进一步封装为通用 HTML 解码模块,例如:

export function decode(raw: string, mode: 'text' | 'attr' = 'text') {
  return decodeHtmlBrowser(raw, mode === 'attr')
}

或添加 SSR 支持(Node 环境下使用第三方库 he)。


七、潜在问题与安全性

  1. XSS 风险
    raw 来自用户输入,直接注入到 innerHTML 可能带来风险(尤其在属性模式下)。
  2. Node 环境不可用
    函数依赖 document,只能在浏览器执行。
  3. 多线程冲突
    在并发场景(如 Web Worker)中,全局 decoder 不安全。

✅ 建议在浏览器端、受控输入场景中使用。


八、总结

decodeHtmlBrowser 是一个利用浏览器解析器进行 HTML 实体解码的小巧函数。
它通过创建一次性 DOM 节点,实现了兼顾性能与简洁的解码逻辑,在前端框架源码(如 Vue、React DOM 工具层)中也可见类似实现。

本质思想:让浏览器帮我们做浏览器最擅长的事——解析 HTML。


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

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

一、背景概念

在 Vue 模板编译阶段,指令(如 v-ifv-forv-text 等)会被转换成相应的 JavaScript 渲染代码。
v-text 是一个较为简单的指令,它的作用是在渲染时设置元素的 textContent 属性

举个例子:

<span v-text="message"></span>

最终会被编译为类似:

_textContent: _toDisplayString(message)

而这一编译行为,正是由编译器内部的 指令转换器(DirectiveTransform) 来完成的,
transformVText 就是处理 v-text 指令的核心逻辑。


二、原理解析

Vue 在编译模板时,会为每种指令注册一个 DirectiveTransform
该函数接受三个参数:

  • dir: 当前指令对象(包含表达式、参数、修饰符等信息)
  • node: 指令所在的节点
  • context: 编译上下文(提供错误处理、工具方法、helper 引用等)

返回值通常是一个对象 { props }
代表该指令将会生成哪些渲染属性(例如 textContentinnerHTML 等)。


三、源码逐行拆解与注释

以下是完整源码及详细注释:

import {
  type DirectiveTransform,
  TO_DISPLAY_STRING,
  createCallExpression,
  createObjectProperty,
  createSimpleExpression,
  getConstantType,
} from '@vue/compiler-core'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'

说明:

  • 导入 DirectiveTransform 类型用于定义函数签名;
  • TO_DISPLAY_STRING 是 Vue 的内部 helper,用于将任意值安全地转换为字符串(即 _toDisplayString());
  • create* 系列函数用于创建 AST 节点(编译阶段的抽象语法树节点);
  • DOMErrorCodescreateDOMCompilerError 用于在编译错误时抛出友好的错误提示。

核心函数定义

export const transformVText: DirectiveTransform = (dir, node, context) => {
  const { exp, loc } = dir
  • dir 是当前的指令描述对象;
  • exp 表示 v-text 的绑定表达式(例如 message);
  • loc 是位置信息,用于报错时定位。

错误检查一:缺少表达式

  if (!exp) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_TEXT_NO_EXPRESSION, loc),
    )
  }

v-text 必须有表达式,否则报错:

<div v-text></div> <!-- ❌ 错误:缺少表达式 -->

错误检查二:存在子节点

  if (node.children.length) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_TEXT_WITH_CHILDREN, loc),
    )
    node.children.length = 0
  }

v-text 会覆盖整个元素的文本内容,因此不允许与子节点共存
例如:

<div v-text="msg">Hello</div> <!-- ❌ 同时存在子节点 -->

编译器会清空子节点,以保证 textContent 是唯一内容。


构造编译结果

  return {
    props: [
      createObjectProperty(
        createSimpleExpression(`textContent`, true),

创建一个对象属性:

{ textContent: <表达式> }

动态或常量表达式判断

        exp
          ? getConstantType(exp, context) > 0
            ? exp
            : createCallExpression(
                context.helperString(TO_DISPLAY_STRING),
                [exp],
                loc,
              )
          : createSimpleExpression('', true),

这里是整个函数的逻辑核心

  • 如果有表达式 exp

    • 判断其是否为常量(getConstantType > 0):

      • ✅ 是常量 → 直接使用;
      • ❌ 否则 → 包装成 _toDisplayString(exp)
  • 如果没有表达式 → 使用空字符串。

例如:

<span v-text="'Hello'"></span>  // 常量 → textContent: 'Hello'
<span v-text="msg"></span>      // 动态 → textContent: _toDisplayString(msg)

尾部返回

      ),
    ],
  }
}

最终返回一个 { props: [...] } 对象,供上层编译逻辑整合到 codegenNode 中,
最终生成渲染函数中对 textContent 的赋值语句。


四、与其它指令的对比

指令 作用 编译生成属性 支持表达式类型
v-text 设置 textContent { textContent: exp } 表达式
v-html 设置 innerHTML { innerHTML: exp } 表达式
v-bind 绑定任意属性 { attr: exp } 表达式
v-on 绑定事件 { onXxx: handler } 函数或表达式

可见,v-text 的核心在于确保内容安全且简单替换,而不像 v-html 那样存在 XSS 风险。


五、实践意义

该转换器的存在使 Vue 模板编译具备以下优点:

  1. AST 层清晰职责分离:模板转换与渲染生成解耦;
  2. 运行时性能优化:常量表达式可直接内联;
  3. 错误捕获机制:在编译阶段即可发现模板误用;
  4. 统一字符串转义逻辑:通过 _toDisplayString() 确保渲染输出安全。

六、拓展与潜在问题

🔹 拓展方向

  • 你可以基于该模式自定义指令转换器,例如:

    • v-markdown → 自动解析 Markdown 内容;
    • v-textsafe → 输出前进行转义与敏感词过滤。

🔹 潜在问题

  • 若用户在模板中误用 v-text 并手动修改 DOM,可能引发内容覆盖;
  • 对性能要求高的场景,应尽量减少 _toDisplayString() 的调用次数;
  • 过度依赖编译时检查,可能忽略运行时动态表达式边界问题。

七、总结

transformVText 是 Vue 编译器中处理 v-text 指令的关键逻辑。
它体现了 Vue 编译体系的核心特征:静态分析、错误预防与安全渲染
通过这一机制,Vue 能在编译阶段就生成高效、安全的渲染函数。


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

深度解析 Vue 编译器中的 transformShow:v-show 指令的编译原理

一、概念背景

v-show 是 Vue 模板系统中的一个常见指令,用于基于布尔条件控制元素的显示状态。与 v-if 不同,v-show 并不会销毁或重新创建 DOM 元素,而是通过动态修改元素的 display 样式属性来实现显隐切换。

在 Vue 的编译器阶段,每一个模板指令(如 v-ifv-forv-onv-bindv-show 等)都会被转换(transform)成对应的运行时代码。本文聚焦于 transformShow 这个编译阶段的指令转换函数。


二、源码解读

下面是 transformShow 的源码(来自 Vue 的 DOM 编译模块):

import type { DirectiveTransform } from '@vue/compiler-core'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'
import { V_SHOW } from '../runtimeHelpers'

export const transformShow: DirectiveTransform = (dir, node, context) => {
  const { exp, loc } = dir
  if (!exp) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, loc),
    )
  }

  return {
    props: [],
    needRuntime: context.helper(V_SHOW),
  }
}

三、逐行解析与原理讲解

1. 引入依赖

import type { DirectiveTransform } from '@vue/compiler-core'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'
import { V_SHOW } from '../runtimeHelpers'
  • DirectiveTransform:类型定义,用于声明一个“指令转换函数”的标准结构。
    它的签名通常是 (dir, node, context) => TransformResult
  • createDOMCompilerError:用于在编译阶段报告错误,比如指令缺少必要参数时。
  • V_SHOW:指向一个运行时帮助函数(runtime helper),即真正执行 v-show 逻辑的部分。

注释说明:
Vue 在编译模板时,会将指令编译为渲染函数调用。在运行时阶段,V_SHOW 对应的函数(位于 runtime-dom)负责实际地更新元素的显示状态。


2. 定义指令转换函数

export const transformShow: DirectiveTransform = (dir, node, context) => {

这段代码定义了一个指令转换器函数。它接收三个参数:

  • dir:当前指令节点对象,包含 nameexp(表达式)、modifiersloc(源码位置信息)等;
  • node:AST 节点(如一个 <div><button> 元素);
  • context:编译上下文,提供错误处理、运行时帮助注册等工具。

3. 取出指令表达式

const { exp, loc } = dir
  • expv-show 后面的表达式,如 v-show="isVisible"
  • loc:源代码位置,用于在错误提示中提供文件行号与列号。

4. 错误检查逻辑

if (!exp) {
  context.onError(
    createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, loc),
  )
}

如果用户写了一个不完整的指令,比如:

<div v-show></div>

则没有提供表达式。此时编译器会调用 context.onError() 触发一个编译错误:

错误信息示例:
[Vue compiler]: v-show is missing expression at line 10:5

这保证了模板语法的正确性,防止运行时报错。


5. 返回转换结果

return {
  props: [],
  needRuntime: context.helper(V_SHOW),
}

这一步是关键。编译器最终需要返回一个结果对象,告诉生成器:

  • props: []
    说明 v-show 不会生成任何静态属性,而是完全交由运行时控制。
  • needRuntime: context.helper(V_SHOW)
    表示该指令在运行时需要 V_SHOW 这个辅助函数。

运行时对应逻辑(位于 runtime-dom):

export const vShow = {
  beforeMount(el, { value }) {
    el.style.display = value ? '' : 'none'
  },
  updated(el, { value, oldValue }) {
    if (value !== oldValue) {
      el.style.display = value ? '' : 'none'
    }
  }
}

编译器阶段只标记“需要此运行时函数”,而不参与实现显示逻辑。


四、与其他指令的对比

指令 是否生成 props 是否需要 runtime helper 行为特征
v-if ✅ 是 ❌ 否(直接编译成条件分支) 通过条件 AST 控制渲染结构
v-on ✅ 是 ✅ 是 绑定事件监听器
v-bind ✅ 是 ❌ 否 绑定动态属性
v-show ❌ 否 ✅ 是 (V_SHOW) 通过样式控制显隐

可以看出,v-showv-if 的根本区别在于运行时行为v-show 属于“渲染后控制”,而非“结构性编译控制”。


五、实践示例:编译结果分析

示例模板:

<div v-show="isVisible"></div>

编译后的伪代码(简化形式):

import { vShow as _vShow } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", {
    directives: [[_vShow, _ctx.isVisible]]
  }))
}

可以看到,_vShow 被注册为运行时指令,并应用于元素的指令数组中。
编译器只是告诉生成器“需要 _vShow”,而不关心具体实现。


六、拓展与思考

1. 为什么不在编译期直接处理?

v-show 的逻辑依赖于 运行时状态(如响应式数据) 。编译时无法确定 isVisible 的值,因此只能延迟到运行时由指令处理。

2. 为什么返回空 props

v-show 不直接修改节点属性,而是通过运行时访问 el.style。因此,编译器无需生成静态绑定。

3. 优化方向

在 SSR 场景下,v-show 可优化为在初始渲染时直接添加 display: none,避免首屏闪烁,这部分由 SSR 编译器自动完成。


七、潜在问题与注意事项

  1. 性能影响
    v-show 在 DOM 中保留元素,因此频繁切换时比 v-if 更高效,但首次渲染时会渲染所有元素。
  2. 样式干扰
    如果手动操作元素的 display 属性,可能与 v-show 的逻辑冲突。
  3. 过渡动画
    v-show 可与 transition 一起使用,但动画实现依赖于 CSS display 切换。

八、总结

transformShow 是 Vue 编译器中极简却关键的一环。
它的职责仅是:

  1. 校验语法合法性;
  2. 注册运行时指令依赖;
  3. 将逻辑委托给运行时的 vShow 实现。

这种编译器-运行时分层设计,体现了 Vue 体系中“轻编译、强运行”的设计哲学。


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

看了下昨日泄露的苹果 App Store 源码……

新闻

昨日苹果 App Store 前端源码泄露,因其生产环境忘记关闭 Sourcemap,被用户下载了源码,上传到 Github。

仓库地址:github.com/rxliuli/app…

目前已经 Fork 和 Star 超 5k:

如果你想要第一时间知道前端资讯,欢迎关注公众号:冴羽

用户如何抓取的源码?

用户 rxliuli 使用 Chrome 插件 Save All Resources 将代码下载了下来。

插件地址为:chromewebstore.google.com/detail/save…

下次你也可以打包下载源码了~

如何看待源码泄露?

其实前端源码泄露对业务本身并没有什么影响,因为前端代码无论是否压缩还是混淆,最终都需传输到浏览器才能运行,本身就具有 “暴露” 属性,SourceMap 只是让代码更易读,更容易调试。

尽管如此,依然不建议在生产环境开启 SourceMap,对普通用户无益,且存在轻微性能开销和源代码暴露的安全风险。

我大致看了下代码,并没有什么密钥之类的信息,所以干点坏事之类的就不用想了。真正有价值的核心代码比如推荐逻辑还是在服务端。

代码使用 Svelte?

我万万没想到,项目使用的是 Svelte。

Svelte 我自然是很熟的,毕竟我翻译过 Svelte 官网:svelte.yayujs.com/

还写了一本掘金小册《Svelte 开发指南》:s.juejin.cn/ds/QNzfZ4eq…

想一想,使用 Svelte 也在情理之中。

因为 Svelte 就非常适合处理这种页面相对简单、业务逻辑并不复杂的页面。

在实现上 ,与其说 Svelte 是框架,不如说 Svelte 是一个编译器。 它会在构建时就会将代码编译为高效的 JavaScript 代码,因此能够实现高性能的 Web 应用。

Svelte 的核心优势在于:

  • 轻量级:核心库只有 3 KB,非常适合开发轻量级项目
  • 高性能:构建时优化,而且不使用虚拟 DOM,减少了内存占用和开销,性能更高
  • 易上手:学习曲线小,入门门槛低,语法简洁易懂

简而言之,Svelte 非常适合构建轻量级 Web 项目,也是本人做个人项目的首选技术栈。

以后大家如果要做相对简单的项目,又有性能上的追求(比如 KPI),那就可以考虑使用 Svelte。

用它作为示例学 Svelte ?

我看了下代码,项目代码还是 Svelte 4,而 Svelte 已经到 5 了,Svelte 4 和 5 不论是底层架构还是基础语法都发生了很大的变化,其变化的剧烈程度类似于 Next.js 12 升 Next.js 13,所以想通过这个项目学习 Svelte 就不用想了,都是些过时的语法了,不如直接学 Svelte 5。

深入解析 Vue 3 编译器中的 transformOn:事件指令的编译机制

在 Vue 的编译阶段,v-on 指令(即 @click@keydown 等事件绑定)并不是简单地原样输出,而是经过编译器的语法树(AST)转换,生成高效的运行时代码。本文将深入解析 Vue 3 源码中的 transformOn 模块,了解它如何处理事件修饰符与动态事件绑定。


一、概念

在 Vue 中,v-on 指令不仅用于绑定事件,还支持一系列修饰符,例如:

<button @click.stop.prevent="onClick">Click Me</button>

这些修饰符会改变事件监听行为,比如:

  • .stop → 调用 event.stopPropagation()
  • .prevent → 调用 event.preventDefault()
  • .once → 只触发一次
  • .capture → 捕获阶段触发

编译器必须将这些声明式修饰符转化为等价的 JavaScript 调用逻辑。
而这正是 transformOn 的职责所在。


二、原理

transformOn 是一个 指令转换器(DirectiveTransform) ,作用于 v-on 相关指令。它主要分为三个阶段:

  1. 基础转换:调用 baseTransform(基础事件转换函数),生成初步的 key(事件名)与 handlerExp(事件处理函数表达式)。

  2. 修饰符分类与处理:通过 resolveModifiers 将所有修饰符分类为:

    • eventOptionModifiers → 事件选项(once, passive, capture)
    • nonKeyModifiers → 非键盘类运行时修饰符(stop, prevent, self, ctrl...)
    • keyModifiers → 键盘事件修饰符(enter, esc, left, right...)
  3. 包装与修正:根据分类结果生成最终的 createObjectProperty(key, handlerExp) 对象。


三、代码拆解与注释

下面逐段分析 transformOn.ts 中的关键实现。


1️⃣ 修饰符类型定义

const isEventOptionModifier = makeMap(`passive,once,capture`)
const isNonKeyModifier = makeMap(
  `stop,prevent,self,ctrl,shift,alt,meta,exact,middle`,
)
const maybeKeyModifier = makeMap('left,right')
const isKeyboardEvent = makeMap(`onkeyup,onkeydown,onkeypress`)

解释:

  • makeMap 用于创建一个哈希表映射,提高修饰符查找效率。

  • Vue 将修饰符分为三类:

    • 事件选项修饰符:直接影响 addEventListener
    • 非键盘修饰符:用于通用事件过滤。
    • 键盘相关修饰符:仅在键盘事件中起作用。

2️⃣ 修饰符分类函数 resolveModifiers

const resolveModifiers = (key, modifiers, context, loc) => {
  const keyModifiers = []
  const nonKeyModifiers = []
  const eventOptionModifiers = []

  for (let i = 0; i < modifiers.length; i++) {
    const modifier = modifiers[i].content

    if (isEventOptionModifier(modifier)) {
      eventOptionModifiers.push(modifier)
    } else if (maybeKeyModifier(modifier)) {
      if (isStaticExp(key)) {
        if (isKeyboardEvent(key.content.toLowerCase())) {
          keyModifiers.push(modifier)
        } else {
          nonKeyModifiers.push(modifier)
        }
      } else {
        keyModifiers.push(modifier)
        nonKeyModifiers.push(modifier)
      }
    } else {
      if (isNonKeyModifier(modifier)) {
        nonKeyModifiers.push(modifier)
      } else {
        keyModifiers.push(modifier)
      }
    }
  }

  return { keyModifiers, nonKeyModifiers, eventOptionModifiers }
}

解释与逻辑注释:

  1. 遍历每个修饰符;

  2. 判断其所属类别:

    • 若是 passive/once/capture → 加入 eventOptionModifiers
    • 若可能是键或鼠标方向(如 left/right),则进一步判断事件名;
    • 其他修饰符通过 isNonKeyModifier 判断是否属于通用行为。
  3. 返回三类结果,供后续调用阶段使用。

这一函数的作用相当于为“修饰符分流”,为后续包装提供信息。


3️⃣ 事件名标准化函数 transformClick

const transformClick = (key: ExpressionNode, event: string) => {
  const isStaticClick =
    isStaticExp(key) && key.content.toLowerCase() === 'onclick'
  return isStaticClick
    ? createSimpleExpression(event, true)
    : key.type !== NodeTypes.SIMPLE_EXPRESSION
      ? createCompoundExpression([
          `(`,
          key,
          `) === "onClick" ? "${event}" : (`,
          key,
          `)`,
        ])
      : key
}

功能:

  • .right.middle 点击事件转换为等价事件:

    • @click.rightonContextmenu
    • @click.middleonMouseup
  • 若事件是动态绑定,则构造条件表达式以在运行时判断。


4️⃣ 主体函数 transformOn

export const transformOn: DirectiveTransform = (dir, node, context) => {
  return baseTransform(dir, node, context, baseResult => {
    const { modifiers } = dir
    if (!modifiers.length) return baseResult

    let { key, value: handlerExp } = baseResult.props[0]
    const { keyModifiers, nonKeyModifiers, eventOptionModifiers } =
      resolveModifiers(key, modifiers, context, dir.loc)

    if (nonKeyModifiers.includes('right')) {
      key = transformClick(key, `onContextmenu`)
    }
    if (nonKeyModifiers.includes('middle')) {
      key = transformClick(key, `onMouseup`)
    }

    if (nonKeyModifiers.length) {
      handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [        handlerExp,        JSON.stringify(nonKeyModifiers),      ])
    }

    if (
      keyModifiers.length &&
      (!isStaticExp(key) || isKeyboardEvent(key.content.toLowerCase()))
    ) {
      handlerExp = createCallExpression(context.helper(V_ON_WITH_KEYS), [        handlerExp,        JSON.stringify(keyModifiers),      ])
    }

    if (eventOptionModifiers.length) {
      const modifierPostfix = eventOptionModifiers.map(capitalize).join('')
      key = isStaticExp(key)
        ? createSimpleExpression(`${key.content}${modifierPostfix}`, true)
        : createCompoundExpression([`(`, key, `) + "${modifierPostfix}"`])
    }

    return { props: [createObjectProperty(key, handlerExp)] }
  })
}

🔍 逐步解读:

  1. 基础转换调用
    通过 baseTransform 获取事件名与处理函数表达式。

  2. 分类解析修饰符
    调用 resolveModifiers 返回三类修饰符集合。

  3. 修饰符应用顺序

    • .right.middle → 改写事件名;
    • 非键盘修饰符 → 包装 V_ON_WITH_MODIFIERS
    • 键盘修饰符 → 包装 V_ON_WITH_KEYS
    • 事件选项修饰符 → 改写事件名后缀(如 onClickOnce)。
  4. 最终返回结构

    return {
      props: [createObjectProperty(key, handlerExp)]
    }
    

    生成 AST 节点形式的属性键值对,用于后续代码生成阶段(Codegen)。


四、实践示例

Vue 模板

<button @click.stop.once="submitForm">Submit</button>

编译后伪代码(简化)

{
  onClickOnce: _withModifiers(submitForm, ["stop"])
}

此处 _withModifiers_withKeys 均由运行时辅助函数实现。


五、拓展:运行时辅助函数

在运行时阶段:

  • V_ON_WITH_MODIFIERS_withModifiers(fn, ["stop", "prevent"])
  • V_ON_WITH_KEYS_withKeys(fn, ["enter", "esc"])

它们会返回一个新函数,在事件触发时根据修饰符自动调用 event.stopPropagation() 等操作。
这实现了 “声明式语法 → 运行时行为” 的无缝衔接。


六、潜在问题与优化方向

  1. 修饰符冲突

    • 某些修饰符组合(如 .exact.ctrl)在动态事件下的行为可能难以预测。
  2. 动态事件名

    • 当事件名不是静态字符串(例如 @[eventName]="fn")时,编译时难以推断事件类型,需要运行时判断。
  3. 性能考虑

    • 每个 _withModifiers 包装都会创建新的函数对象;在大规模动态列表中可能增加内存消耗。
  4. 代码生成阶段的优化

    • 可通过静态分析提前合并部分修饰符逻辑,减少运行时代码体积。

七、总结

transformOn 是 Vue 编译器中极具代表性的模块之一:

  • 它展现了 Vue 编译期指令重写 的设计哲学;
  • 将模板语法中的声明式修饰符,转化为最小化的运行时代码;
  • 通过多层函数封装,实现灵活而一致的事件行为。

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

Vue 模板编译器中的 transformModel:v-model 指令的编译秘密

v-model 是 Vue 中最具代表性的双向绑定语法糖,它在运行时能自动管理表单输入与数据之间的同步。而在编译阶段,Vue 的模板编译器(@vue/compiler-dom)通过 transformModel 函数将 v-model 转换为运行时可识别的指令表达式。

本文我们将深入剖析源码:

import {
  type DirectiveTransform,
  ElementTypes,
  NodeTypes,
  transformModel as baseTransform,
  findDir,
  findProp,
  hasDynamicKeyVBind,
  isStaticArgOf,
} from '@vue/compiler-core'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'
import {
  V_MODEL_CHECKBOX,
  V_MODEL_DYNAMIC,
  V_MODEL_RADIO,
  V_MODEL_SELECT,
  V_MODEL_TEXT,
} from '../runtimeHelpers'

一、概念:transformModel 的角色定位

在 Vue 编译过程中,指令(如 v-modelv-htmlv-bind)都会被编译器的 transform 阶段 转换成适合运行时的结构。
transformModel 是 DOM 编译器中专门处理 v-model 的指令转换函数,目标是:

  1. 判断绑定的元素类型(如 <input><select>)。
  2. 检查错误用法(如 v-model 绑定到文件输入)。
  3. 注入运行时辅助函数(如 V_MODEL_TEXTV_MODEL_CHECKBOX 等)。

二、原理:编译时如何决定不同的绑定逻辑

1️⃣ 调用基础转换逻辑

const baseResult = baseTransform(dir, node, context)
  • 这里调用了核心模块 @vue/compiler-core 的通用版本 transformModel
  • 它会生成一个基础结果对象 { props, needRuntime },为后续 DOM 特有的逻辑扩展打底。

2️⃣ 组件与普通元素的区分

if (!baseResult.props.length || node.tagType === ElementTypes.COMPONENT) {
  return baseResult
}

解释:

  • 如果 v-model 是在组件上(例如 <MyInput v-model="x" />),编译器不会做额外 DOM 层级转换,只保留基础属性。
  • 普通元素则继续深入检查类型。

3️⃣ 检查非法参数使用

if (dir.arg) {
  context.onError(
    createDOMCompilerError(
      DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
      dir.arg.loc,
    ),
  )
}

v-model:foo="x" 这种语法仅对组件有效,对原生元素无意义。


4️⃣ 检查重复绑定 value

function checkDuplicatedValue() {
  const value = findDir(node, 'bind')
  if (value && isStaticArgOf(value.arg, 'value')) {
    context.onError(
      createDOMCompilerError(
        DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
        value.loc,
      ),
    )
  }
}

当用户在使用 v-model 的同时又写了 :value 时,可能造成冲突或冗余,因此编译器在开发模式下会发出警告。


5️⃣ 识别元素类型并选择合适的运行时助手

const { tag } = node
const isCustomElement = context.isCustomElement(tag)

Vue 会为不同类型的元素绑定不同的指令运行时函数:

元素类型 对应运行时辅助符号 功能说明
<input type="text"> V_MODEL_TEXT 文本输入双向绑定
<input type="checkbox"> V_MODEL_CHECKBOX 多选框绑定
<input type="radio"> V_MODEL_RADIO 单选绑定
<select> V_MODEL_SELECT 下拉选择绑定
自定义组件或动态 :type V_MODEL_DYNAMIC 动态类型运行时绑定

源码逻辑如下:

let directiveToUse = V_MODEL_TEXT
let isInvalidType = false

if (tag === 'input' || isCustomElement) {
  const type = findProp(node, `type`)
  if (type) {
    if (type.type === NodeTypes.DIRECTIVE) {
      directiveToUse = V_MODEL_DYNAMIC
    } else if (type.value) {
      switch (type.value.content) {
        case 'radio':
          directiveToUse = V_MODEL_RADIO
          break
        case 'checkbox':
          directiveToUse = V_MODEL_CHECKBOX
          break
        case 'file':
          isInvalidType = true
          context.onError(
            createDOMCompilerError(
              DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
              dir.loc,
            ),
          )
          break
        default:
          __DEV__ && checkDuplicatedValue()
      }
    }
  } else if (hasDynamicKeyVBind(node)) {
    directiveToUse = V_MODEL_DYNAMIC
  } else {
    __DEV__ && checkDuplicatedValue()
  }
} else if (tag === 'select') {
  directiveToUse = V_MODEL_SELECT
} else {
  __DEV__ && checkDuplicatedValue()
}

6️⃣ 注入运行时指令引用

if (!isInvalidType) {
  baseResult.needRuntime = context.helper(directiveToUse)
}

此时,baseResult.needRuntime 会携带一个对运行时 resolveDirective() 的引用,使生成的渲染函数能在运行时调用正确的指令处理逻辑。


7️⃣ 移除编译期无用的 modelValue 属性

baseResult.props = baseResult.props.filter(
  p => !(p.key.type === NodeTypes.SIMPLE_EXPRESSION && p.key.content === 'modelValue')
)

原因:原生元素的 v-model 不需要显式的 modelValue 传入,它会在运行时通过 binding.value 自动管理,因此删除以减少代码体积。


三、对比:@vue/compiler-core@vue/compiler-dom

  • @vue/compiler-core:负责通用的 AST 构建与基础转换(组件/指令通用逻辑)。

  • @vue/compiler-dom:在此基础上为浏览器平台添加 DOM 特有的行为,例如:

    • 检查 <input type="file"> 这种非法绑定。
    • 区分 checkboxradio 的运行时指令。
    • 针对开发模式(__DEV__)进行额外校验。

四、实践:transformModel 实际输出示例

当编译如下模板:

<input v-model="msg" type="text">

编译结果(简化版)大致为:

{
  props: [],
  needRuntime: helper(V_MODEL_TEXT)
}

渲染函数中会生成:

withDirectives(
  createElementVNode("input", null, null, 512 /* NEED_PATCH */),
  [[vModelText, msg]]
)

最终由 vModelText 在运行时处理 input 的输入/输出同步。


五、拓展:动态输入类型的特殊处理

例如:

<input :type="inputType" v-model="value">

此时编译器会检测到 type 是一个动态绑定(NodeTypes.DIRECTIVE),
自动切换到:

directiveToUse = V_MODEL_DYNAMIC

运行时则会在输入类型变化时动态切换不同的监听逻辑。


六、潜在问题与边界

  1. 文件输入限制
    v-model 不支持 <input type="file">,必须使用事件监听手动处理上传。
  2. 重复绑定冲突
    若同时使用 :valuev-model,可能导致值不一致问题。
  3. 自定义元素兼容性
    对于 Web Components,需自定义 isCustomElement 逻辑,保证 v-model 的行为一致。

七、总结

transformModel 是 Vue 模板编译器中将 “语法糖” 翻译为 “运行时逻辑” 的关键节点。
它体现了 Vue 的一个核心设计哲学——在编译阶段智能决策,在运行时高效执行

在理解了这段代码后,你不仅能掌握 v-model 的编译机制,还能更好地理解 Vue 模板编译的抽象层次:
从语法到 AST,从 AST 到渲染函数,再从渲染函数到最终 DOM 更新。


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

深入理解 Vue 编译阶段的 v-html 指令转换逻辑

在 Vue 的模板编译过程中,v-html 是一个特殊的 DOM 指令,它允许开发者直接将一段字符串内容设置为元素的 innerHTML。这篇文章将从源码角度解析 transformVHtml 的实现逻辑,理解其背后的安全约束与编译策略。


一、背景与概念

v-html 在 Vue 中的用途是让开发者能够动态插入一段 HTML 内容,例如:

<div v-html="rawHtml"></div>

这段代码在运行时会把 rawHtml 的字符串直接作为 innerHTML 写入 <div> 元素中。
在编译器阶段,Vue 会将该指令转换为渲染函数可识别的属性设置表达式。
例如:

{ innerHTML: rawHtml }

而整个转换的逻辑就集中在 transformVHtml 这个指令转换函数中。


二、源码结构与实现

import {
  type DirectiveTransform,
  createObjectProperty,
  createSimpleExpression,
} from '@vue/compiler-core'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'

export const transformVHtml: DirectiveTransform = (dir, node, context) => {
  const { exp, loc } = dir
  if (!exp) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_HTML_NO_EXPRESSION, loc),
    )
  }
  if (node.children.length) {
    context.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_HTML_WITH_CHILDREN, loc),
    )
    node.children.length = 0
  }
  return {
    props: [
      createObjectProperty(
        createSimpleExpression(`innerHTML`, true, loc),
        exp || createSimpleExpression('', true),
      ),
    ],
  }
}

三、源码逐行解析与注释

1. 导入依赖

import {
  type DirectiveTransform,
  createObjectProperty,
  createSimpleExpression,
} from '@vue/compiler-core'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'
  • DirectiveTransform:定义了一个指令转换函数的类型签名。Vue 在编译模板时,会为每个指令(如 v-if, v-for, v-html)注册对应的转换逻辑。
  • createObjectProperty:用于生成对象属性的 AST 节点。
  • createSimpleExpression:生成简单表达式节点(如字符串字面量或变量引用)。
  • createDOMCompilerError:在 DOM 转换阶段生成编译错误信息。

2. 定义主函数

export const transformVHtml: DirectiveTransform = (dir, node, context) => {

此处声明了一个指令转换函数 transformVHtml,其签名固定为 (dir, node, context)

  • dir:当前指令节点信息(包含表达式、参数、修饰符等)。
  • node:所在元素节点。
  • context:编译上下文,提供错误报告与代码生成工具。

3. 检查表达式有效性

const { exp, loc } = dir
if (!exp) {
  context.onError(
    createDOMCompilerError(DOMErrorCodes.X_V_HTML_NO_EXPRESSION, loc),
  )
}

逻辑说明:

  • v-html 必须绑定一个表达式(例如 v-html="htmlContent")。
  • 若表达式缺失(如 v-html 单独存在),则调用 context.onError 抛出错误。
  • 这里的错误类型为 X_V_HTML_NO_EXPRESSION

设计思路:
Vue 编译器会严格要求 v-html 提供动态值,否则模板含义不明确,无法生成有效的渲染代码。


4. 检查子节点冲突

if (node.children.length) {
  context.onError(
    createDOMCompilerError(DOMErrorCodes.X_V_HTML_WITH_CHILDREN, loc),
  )
  node.children.length = 0
}

逻辑说明:

  • 如果一个元素已经使用了 v-html,它的子节点将被完全替换,因此原始子节点在模板中是无效的。

  • 这段代码检测 node.children 是否非空;若存在,则:

    1. 报错提示开发者(X_V_HTML_WITH_CHILDREN)。
    2. 清空所有子节点,防止生成冲突的渲染逻辑。

示例错误:

<div v-html="rawHtml">
  <p>这段内容将被覆盖</p>
</div>

5. 生成最终的 AST 转换结果

return {
  props: [
    createObjectProperty(
      createSimpleExpression(`innerHTML`, true, loc),
      exp || createSimpleExpression('', true),
    ),
  ],
}

关键逻辑:

  • 返回的对象告诉编译器:
    该指令应转换为一个 props(即元素属性)数组。
  • createObjectProperty() 的作用是生成 { innerHTML: exp } 的 AST 表达形式。
  • exp 不存在,则使用空字符串表达式占位,防止后续阶段崩溃。

结果示例:

输入模板:

<div v-html="content"></div>

编译输出(简化):

{
  props: [{ key: 'innerHTML', value: content }]
}

这在渲染函数中最终转化为:

el.innerHTML = content

四、设计原理与对比

特性 v-html {{ }} 插值表达式
内容类型 原始 HTML 字符串 纯文本(HTML 转义)
安全性 潜在 XSS 风险 自动转义,安全
编译输出 innerHTML = exp textContent = exp
子节点 被清空 可混合使用

对比总结:

  • v-html 是“危险操作”,适用于可信内容(例如 CMS 返回的安全 HTML)。
  • 插值表达式自动防止注入攻击,推荐默认使用。

五、实践建议

  1. 仅在必要时使用 v-html:若只是输出文本,应使用 {{ }}
  2. 对内容进行清洗:例如使用 DOMPurify 过滤 HTML。
  3. 避免动态注入用户输入:防止跨站脚本(XSS)攻击。
  4. 注意 SSR 一致性innerHTML 可能导致服务端与客户端不一致。

六、拓展思考

1. 在编译管线中的位置

transformVHtml 属于 DOM 级别指令转换,它在模板编译第二阶段(node transform 阶段)执行,属于 结构性重写 类型的变换逻辑。

2. 可扩展性

开发者可参考其实现方式,创建自定义指令的编译时转换逻辑,通过 DirectiveTransform 接口将指令映射为目标属性或指令调用。


七、潜在问题与改进方向

  1. 安全风险:直接操作 innerHTML 无法防止恶意脚本注入。
  2. 性能问题:频繁更改 innerHTML 会导致 DOM 重绘。
  3. 无法绑定事件:通过 v-html 注入的内容不会被 Vue 模板编译处理。

Vue 团队在设计上有意将 v-html 视为“逃逸阀门”,仅用于特定、可信的场景。


八、总结

transformVHtml 是 Vue 编译器中处理 v-html 指令的核心函数,它的职责不仅是生成 innerHTML 属性绑定,同时还负责在编译阶段进行安全校验和错误提示。通过它,我们能直观地看到 Vue 如何在编译期约束开发者行为,保证运行时的正确性与安全性。


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

Vue 模板编译中的 HTML 嵌套验证机制:validateHtmlNesting 源码解析

一、概念:HTML 嵌套校验的意义

在前端框架中,模板编译阶段不仅要生成可运行的渲染函数,还需要在静态分析层面发现潜在错误。
其中一个常见的错误类型是 HTML 标签的非法嵌套
例如:

<p><div>text</div></p>

这种结构在浏览器中虽然可能“看起来能渲染”,但会导致 DOM 结构自动更正,从而引发 Hydration 错误VNode 对比不一致

Vue 在编译模板时,使用 validateHtmlNesting 这个编译阶段钩子(NodeTransform)来提前捕获这种错误。


二、原理:编译器 NodeTransform 机制

Vue 的模板编译分为多个阶段,其中 转换阶段(transform phase) 用来操作抽象语法树(AST)。
在每个节点遍历时,可以通过注册 NodeTransform 来实现不同类型的语义分析或结构优化。

validateHtmlNesting 就是这样一个 AST 节点级转换函数,它的职责是:

  1. 在遇到 HTML 元素节点时;
  2. 检查其父元素;
  3. 判断该父子关系是否符合 HTML 规范;
  4. 如果不符合,则通过 context.onWarn() 发出编译警告。

三、源码与逐行解释

import {
  type CompilerError,
  ElementTypes,
  type NodeTransform,
  NodeTypes,
} from '@vue/compiler-core'
import { isValidHTMLNesting } from '../htmlNesting'

逐行解析:

  • @vue/compiler-core 导入编译器核心类型与常量:

    • CompilerError: 编译错误类型定义;
    • ElementTypes: 元素分类常量(普通元素、组件、slot等);
    • NodeTransform: 转换函数类型;
    • NodeTypes: AST 节点类型常量(文本、注释、元素等)。
  • isValidHTMLNesting: 自定义工具函数,用于校验父子标签的合法性。


export const validateHtmlNesting: NodeTransform = (node, context) => {

解释:
定义并导出一个 NodeTransform 函数,接收两个参数:

  • node: 当前遍历到的 AST 节点;
  • context: 编译上下文,提供如 parentonWarn 等辅助信息。

  if (
    node.type === NodeTypes.ELEMENT &&
    node.tagType === ElementTypes.ELEMENT &&
    context.parent &&
    context.parent.type === NodeTypes.ELEMENT &&
    context.parent.tagType === ElementTypes.ELEMENT &&
    !isValidHTMLNesting(context.parent.tag, node.tag)
  ) {

解释:

这是关键的校验逻辑。条件依次判断:

  1. 当前节点是一个 HTML 元素节点
  2. 父节点存在,且也是一个 HTML 元素节点
  3. isValidHTMLNesting 返回 false —— 即该父子标签不合法。

满足这些条件时,说明出现了非法嵌套。


    const error = new SyntaxError(
      `<${node.tag}> cannot be child of <${context.parent.tag}>, ` +
        'according to HTML specifications. ' +
        'This can cause hydration errors or ' +
        'potentially disrupt future functionality.',
    ) as CompilerError

解释:

构造一个 SyntaxError 对象,并强制转换为 CompilerError 类型。
提示信息明确指出问题标签及可能后果(Hydration 错误、未来功能异常等)。


    error.loc = node.loc

解释:
将错误位置定位(loc)绑定到当前节点,方便在编译器输出中高亮具体行列号。


    context.onWarn(error)
  }
}

解释:

最终通过编译上下文的 onWarn 方法发出警告,而不是直接抛出错误。
这样做的好处是允许编译继续执行,同时向开发者输出非致命问题。


四、对比:与其他框架的实现差异

框架 校验方式 错误处理策略
Vue 3 编译阶段静态分析 + AST 层级检测 context.onWarn 发出警告
React 无编译期检测,依赖运行时渲染结果 依赖浏览器修正或开发警告
Svelte 在编译期直接报错并阻止生成 compiler error 终止输出

Vue 的设计选择了中间方案——保守警告,不强制中断,既确保开发者可见,又不影响正常构建。


五、实践:如何触发与验证

示例模板:

<template>
  <p>
    <div>Invalid Nesting</div>
  </p>
</template>

运行 vue/compiler-sfc 的编译函数后,会触发如下警告:

<p> → <div> nesting is invalid according to HTML specifications.

通过这种机制,开发者能在 IDE 或 CLI 编译时即发现潜在问题。


六、拓展:isValidHTMLNesting 的实现思路

该函数通常通过一组 嵌套规则表 实现,例如:

const invalidPairs = {
  p: ['div', 'section', 'header', 'footer'],
  ul: ['div', 'p'],
  table: ['div', 'p']
}

然后通过:

export function isValidHTMLNesting(parent: string, child: string): boolean {
  return !(invalidPairs[parent]?.includes(child))
}

实现快速验证。
当然,实际 Vue 实现会更复杂,遵循完整的 HTML 语义规则。


七、潜在问题与改进空间

  1. 静态规则局限性
    对自定义组件或 v-if 动态分支结构无法提前判断。
  2. 多层嵌套链分析
    当前只检查直接父子关系,未递归校验祖先节点。
  3. IDE 集成提示增强
    可结合语言服务 (Volar) 提供实时嵌套高亮与自动修复建议。

总结

validateHtmlNesting 是 Vue 编译器中一个小而关键的环节,它在静态分析阶段保证了模板结构的语义正确性。
通过它,框架在编译期就能捕获运行时潜在的结构性错误,大幅提升代码健壮性。


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

Vue Compiler 内部机制解析:transformTransition 源码深度剖析

本文将带你逐层拆解 Vue 编译器核心模块中的 transformTransition 函数,了解它如何在编译阶段识别 <transition> 组件并注入特定属性,从而在运行时实现更高效的过渡逻辑。


一、概念层:什么是 transformTransition

在 Vue 的编译流程中,每个模板节点都会经过一系列的 transform(转换)操作。这些转换器会修改 AST(抽象语法树)节点,使其具备生成最终渲染代码所需的信息。

transformTransition 就是其中之一。它的职责是:

  1. 识别 <transition> 组件;
  2. 检查其子节点的合法性;
  3. 如果子节点使用了 v-show 指令,则自动注入 persisted: true 属性,使过渡在切换显示状态时保持节点的状态。

二、原理层:源码逐行解析

下面我们逐段阅读并注释源码。

1️⃣ 模块导入部分

import {
  type ComponentNode,
  ElementTypes,
  type IfBranchNode,
  type NodeTransform,
  NodeTypes,
} from '@vue/compiler-core'
import { TRANSITION } from '../runtimeHelpers'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'

逐行说明:

  • @vue/compiler-core 导入编译阶段的节点类型定义(如 ComponentNodeNodeTypes)。
  • runtimeHelpers 导入内置标识符 TRANSITION(用于判断是否是 <transition> 组件)。
  • ../errors 导入错误处理工具,用于在发现不合法节点时报告编译错误。

2️⃣ 定义核心转换函数

export const transformTransition: NodeTransform = (node, context) => {

注释:

  • NodeTransform 是编译器在遍历 AST 时调用的钩子函数类型。
  • node 是当前 AST 节点。
  • context 是编译上下文(提供错误报告、组件识别等功能)。

3️⃣ 判断节点是否为内置 <transition> 组件

if (
  node.type === NodeTypes.ELEMENT &&
  node.tagType === ElementTypes.COMPONENT
) {
  const component = context.isBuiltInComponent(node.tag)
  if (component === TRANSITION) {
    return () => {

解析:

  • 首先确认该节点是一个组件类型的元素。
  • 调用 context.isBuiltInComponent() 来检查它是否为内置组件。
  • 若匹配到 TRANSITION,则返回一个“延迟执行的后处理函数”,会在其子节点都处理完之后调用。

4️⃣ 校验子节点合法性

if (!node.children.length) {
  return
}

if (hasMultipleChildren(node)) {
  context.onError(
    createDOMCompilerError(
      DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN,
      {
        start: node.children[0].loc.start,
        end: node.children[node.children.length - 1].loc.end,
        source: '',
      },
    ),
  )
}

解读:

  • <transition> 没有子节点,则无需处理。
  • 若存在多个子节点,则调用 hasMultipleChildren() 检测。
  • 若不合法,则通过 context.onError() 报告错误,提示“transition 只能有一个根元素”。

5️⃣ 检查 v-show 并注入 persisted: true

const child = node.children[0]
if (child.type === NodeTypes.ELEMENT) {
  for (const p of child.props) {
    if (p.type === NodeTypes.DIRECTIVE && p.name === 'show') {
      node.props.push({
        type: NodeTypes.ATTRIBUTE,
        name: 'persisted',
        nameLoc: node.loc,
        value: undefined,
        loc: node.loc,
      })
    }
  }
}

逐行分析:

  • 取出唯一的子节点;
  • 遍历该子节点的所有属性;
  • 若检测到 v-show 指令,则在 <transition> 的 props 中动态添加 persisted 属性。
  • 这样在运行时,过渡组件会保持节点不被销毁,而仅通过 CSS 控制显示状态,从而支持显示/隐藏切换的平滑动画。

6️⃣ 辅助函数:hasMultipleChildren

function hasMultipleChildren(node: ComponentNode | IfBranchNode): boolean {
  const children = (node.children = node.children.filter(
    c =>
      c.type !== NodeTypes.COMMENT &&
      !(c.type === NodeTypes.TEXT && !c.content.trim()),
  ))
  const child = children[0]
  return (
    children.length !== 1 ||
    child.type === NodeTypes.FOR ||
    (child.type === NodeTypes.IF && child.branches.some(hasMultipleChildren))
  )
}

核心逻辑:

  • 过滤掉注释节点和空白文本节点;
  • 检查是否存在多个有效子节点;
  • 若唯一子节点仍是一个 v-forv-if 分支,则递归判断其内部是否存在多个可渲染节点。

三、对比层:与其他编译阶段的关系

模块 功能 相互关系
transformTransition 针对 <transition> 的结构合法性检查与属性注入 属于 DOM 特有 transform
transformElement 通用元素结构转换 会被 transformTransition 之后处理
transformIf 处理 v-if/v-else 结构 可被 hasMultipleChildren 检测
transformShow 处理 v-show 指令 触发 persisted: true 注入逻辑

四、实践层:如何在模板中触发此逻辑

<template>
  <transition>
    <div v-show="visible">Hello Vue</div>
  </transition>
</template>

编译后(简化示意)

_createVNode(Transition, { persisted: true }, [
  _createVNode('div', { style: { display: visible ? '' : 'none' } }, 'Hello Vue')
])

说明:

  • 编译器自动注入 persisted: true
  • 运行时 Transition 组件知道节点不应被销毁,而是仅通过样式切换实现动画。

五、拓展层:为什么 persisted 必要?

v-ifv-show 的区别在于:

  • v-if:销毁与重建 DOM;
  • v-show:仅切换 CSS display

transition 包裹 v-show 元素时,若不设置 persisted,Vue 可能错误地认为节点被卸载,从而导致动画不生效。

因此 persisted 告诉运行时:

“这个节点在逻辑上是同一个,只是暂时隐藏,不要销毁。”


六、潜在问题与改进方向

问题 说明
多子节点报错难调试 若模板动态生成多个节点,错误定位需开发者额外判断。
缺少嵌套提示 <transition> 嵌套在 v-if 中,错误信息较为抽象。
可扩展性有限 无法处理自定义 transition 逻辑(如多节点共享动画)。

未来方向:

  • 提供更详细的错误提示;
  • 支持 <transition-group> 的自动类型检查;
  • 允许开发者通过编译插件扩展 transform 阶段。

总结

transformTransition 是 Vue 编译器中一个精致的小模块,它不直接参与渲染,却决定了 <transition> 组件的行为边界。通过静态分析 AST,它保证:

  • 合法性检查;
  • 动态注入 persisted
  • 提供编译期错误反馈。

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

深度解析 Vue 编译阶段的 transformStyle:从静态 style 到动态绑定的转换逻辑

一、概念篇:编译期处理静态内联样式

在 Vue 的模板编译过程中,静态属性(如 style="color:red")会被视为普通 HTML 属性。然而,为了与动态绑定(如 :style="{ color: 'red' }") 统一处理,Vue 编译器在解析 AST(抽象语法树)阶段会对这些静态样式进行一次“风格转换(transform) ”。

核心目标:
把静态 style 属性转化为可被后续 transformElement 处理的动态绑定指令形式:

<!-- 原始模板 -->
<div style="color: red"></div>

<!-- 编译后效果 -->
<div :style="{ color: 'red' }"></div>

这样做的好处是让静态与动态样式统一进入响应式系统,从而支持更灵活的样式合并和优化。


二、原理篇:NodeTransform 的运行机制

Vue 编译核心提供了多种 Transform Hook 来操作 AST 节点,NodeTransform 就是其中之一。

它的签名如下:

type NodeTransform = (node: RootNode | TemplateChildNode, context: TransformContext) => void

在这里,transformStyle 就是一个符合该类型的函数,它会在遍历 AST 节点时被调用,用于:

  • 判断节点是否为元素节点;
  • 遍历其属性;
  • 找到 style 静态属性;
  • 将其替换成等价的动态绑定。

三、源码篇:transformStyle 逐行解析

import {
  ConstantTypes,
  type NodeTransform,
  NodeTypes,
  type SimpleExpressionNode,
  type SourceLocation,
  createSimpleExpression,
} from '@vue/compiler-core'
import { parseStringStyle } from '@vue/shared'

导入模块说明:

  • NodeTypes:枚举所有 AST 节点类型,如 ELEMENTATTRIBUTEDIRECTIVE
  • ConstantTypes:标识表达式常量类型,用于后续优化。
  • createSimpleExpression:创建表达式节点。
  • parseStringStyle:将 style="..." 字符串解析成对象 { key: value }

核心转换逻辑

export const transformStyle: NodeTransform = node => {
  if (node.type === NodeTypes.ELEMENT) {
    node.props.forEach((p, i) => {
      if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
        node.props[i] = {
          type: NodeTypes.DIRECTIVE,
          name: `bind`,
          arg: createSimpleExpression(`style`, true, p.loc),
          exp: parseInlineCSS(p.value.content, p.loc),
          modifiers: [],
          loc: p.loc,
        }
      }
    })
  }
}

逐行注释:

  1. 判断节点类型:

    if (node.type === NodeTypes.ELEMENT)
    

    仅处理元素节点(<div><span> 等),跳过文本与表达式节点。

  2. 遍历节点属性:

    node.props.forEach((p, i) => { ... })
    

    逐个检查属性是否是 style

  3. 匹配静态 style:

    if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value)
    

    仅处理形如 style="..." 的静态样式。

  4. 替换为动态绑定指令:

    node.props[i] = { type: NodeTypes.DIRECTIVE, name: 'bind', ... }
    

    将属性节点替换为一个“绑定指令节点”,相当于 :style="..."

  5. 创建绑定参数和表达式:

    • arg: createSimpleExpression('style', true, p.loc) → 绑定目标 style
    • exp: parseInlineCSS(p.value.content, p.loc) → 转换 CSS 字符串为对象表达式

四、CSS 解析函数详解

const parseInlineCSS = (
  cssText: string,
  loc: SourceLocation,
): SimpleExpressionNode => {
  const normalized = parseStringStyle(cssText)
  return createSimpleExpression(
    JSON.stringify(normalized),
    false,
    loc,
    ConstantTypes.CAN_STRINGIFY,
  )
}

拆解说明:

  1. parseStringStyle(cssText):将 color: red; font-size: 14px; 解析为:

    { color: 'red', 'font-size': '14px' }
    
  2. JSON.stringify(normalized):生成 "{"color":"red","font-size":"14px"}" 字符串。

  3. createSimpleExpression(..., ConstantTypes.CAN_STRINGIFY)
    表示这是一个可安全序列化为字符串的常量表达式,方便后续优化和缓存。


五、对比篇:与 runtime 的区别

处理阶段 模块 功能
编译阶段 transformStyle 静态 CSS → 动态对象绑定
运行阶段 normalizeStyle(runtime-dom) 合并多种 style 来源(数组/对象/字符串)

✅ 编译期主要负责 转换与静态优化
✅ 运行期则负责 合并与渲染适配

两者的协作实现了 Vue 模板中灵活的样式系统。


六、实践篇:示例演示

输入模板:

<div style="color: red; background: blue"></div>

编译阶段中间产物(简化版 AST):

{
  type: 'ELEMENT',
  props: [
    {
      type: 'DIRECTIVE',
      name: 'bind',
      arg: { content: 'style' },
      exp: { content: '{"color":"red","background":"blue"}' }
    }
  ]
}

最终生成代码:

createElementVNode("div", { style: { color: "red", background: "blue" } })

七、拓展篇:自定义编译器插件思路

开发者可以参考 transformStyle,自定义类似的编译期插件,例如:

  • 自动将静态 class 转化为 :class
  • data-* 属性统一转换为对象;
  • 自定义模板语法扩展。
export const transformDataAttr: NodeTransform = node => {
  if (node.type === NodeTypes.ELEMENT) {
    node.props = node.props.map(p =>
      p.name.startsWith('data-')
        ? {
            ...p,
            type: NodeTypes.DIRECTIVE,
            name: 'bind',
            arg: createSimpleExpression(p.name, true, p.loc),
            exp: createSimpleExpression(JSON.stringify(p.value?.content || ''), false, p.loc)
          }
        : p
    )
  }
}

八、潜在问题与优化空间

问题 说明
⚠️ 无法处理动态 style 值 style="{{ color }}" 不在编译期处理范围。
⚠️ 依赖 parseStringStyle 解析器 对复杂 CSS(如 url()、嵌套语法)支持有限。
⚠️ JSON.stringify 结果非最优 无法进行运行时合并优化,可能会生成重复对象。

改进方向:

  • 在编译期缓存 parseStringStyle 结果;
  • 合并多层 style;
  • 针对响应式场景提供静态/动态混合优化。

九、总结

transformStyle 是 Vue 编译器中一个小而精巧的转换模块。它承担着静态样式语法糖到动态绑定的桥梁作用,让模板编译结果更加统一、可优化、可扩展。

通过它,我们可以更深入理解 Vue 编译阶段的 AST 操作机制、指令生成逻辑以及静态优化策略,为自定义编译器插件或模板 DSL 设计打下坚实基础。


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

Vue 编译器核心模块解读:stringifyStatic 静态节点字符串化机制

一、概念与背景

在 Vue 3 的编译优化体系中,静态提升(Hoisting) 是关键机制之一,它能让模板中的不变内容在渲染时只创建一次,显著减少运行时开销。

然而,Vue 在 Node.js 环境中又进行了更激进的优化——静态节点字符串化(Stringify Static Trees)
其核心逻辑由 stringifyStatic 实现,目标是将可完全静态化的节点块序列转换成 一个字符串形式的静态 vnode

const _hoisted_1 = createStaticVNode(`<div class="foo">bar</div>`)

这种方式使得渲染时仅需通过 innerHTML 插入节点内容,大幅提升 SSR 和 hydration(同构水合)性能。


二、原理解析

1. 整体逻辑流程

stringifyStatic 接收三个参数:

(children, context, parent)

作用是扫描一组编译时的 children(AST 子节点),寻找连续的可静态化节点块,将它们合并为一个静态字符串节点。

核心步骤:

  1. 过滤场景:在 v-slot 范围内直接跳过,因为 slot 内容依赖运行时。

  2. 遍历节点列表:检测每个节点是否“可字符串化”。

  3. 累计计数:记录节点数量 (nc) 与带绑定属性的节点数量 (ec)。

  4. 达到阈值时转换

    • 将一段静态节点块转换为 createStaticVNode 调用;
    • 删除被合并的节点;
    • 更新缓存引用。

转换的触发条件由以下枚举控制:

export enum StringifyThresholds {
  ELEMENT_WITH_BINDING_COUNT = 5,
  NODE_COUNT = 20,
}

2. “可字符串化”节点判定:analyzeNode

analyzeNode(node) 的职责是判断某个节点能否通过字符串安全地重建 DOM:

  • 排除特例

    • 表格类元素(<tr>, <tbody> 等);
    • v-once
    • 运行时常量表达式;
    • 带动态绑定但值无法静态求解。

若节点合法,返回 [节点数, 绑定属性数],否则返回 false

示例:

if (node.type === NodeTypes.ELEMENT && isNonStringifiable(node.tag)) {
  return false
}

通过静态递归检查子节点,确认所有嵌套结构均可被序列化为纯字符串。


3. 字符串化主逻辑:stringifyNode

stringifyNode 会将不同类型的 AST 节点转为 HTML 字符串:

  • 文本节点escapeHtml(content)
  • 注释节点<!--content-->
  • 插值表达式 → 常量求值后转义输出
  • 复合表达式 → 递归求值后拼接
  • 元素节点 → 调用 stringifyElement
switch (node.type) {
  case NodeTypes.ELEMENT:
    return stringifyElement(node, context)
  case NodeTypes.TEXT:
    return escapeHtml(node.content)
  ...
}

4. 元素序列化:stringifyElement

该函数负责拼接元素标签与属性:

let res = `<${node.tag}`

核心逻辑拆解:

  • 遍历属性:

    • 普通属性 → 直接输出;
    • v-bind → 仅常量表达式可保留;
    • v-html / v-text → 解析为 innerHTML 内容;
  • 拼接作用域 ID(scopeId);

  • 子节点递归字符串化;

  • 非自闭合标签补上闭合符号。

示例输出:

<div id="a" class="foo">bar</div>

5. 常量表达式求值:evaluateConstant

在模板中出现的 {{ 1 + 2 }} 等常量插值会在编译时直接执行:

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

⚠️ 安全提示:虽然使用 eval 风格,但 Vue 在上游编译阶段保证这些表达式是常量且无副作用,防止注入攻击。


三、与普通静态提升的对比

对比维度 普通静态提升 字符串化静态提升
存储形式 AST 常量引用 HTML 字符串
渲染方式 createVNode 创建 innerHTML 填充
适用场景 小型或局部静态节点 大块静态结构(如列表)
性能特点 轻度优化 强化版(SSR 友好)
限制条件 较宽松 必须完全无运行时依赖

四、实践案例

假设我们有模板:

<template>
  <div class="foo"><p>static</p><span>content</span></div>
</template>

stringifyStatic 转换后,生成代码类似:

const _hoisted_1 = createStaticVNode(
  `<div class="foo"><p>static</p><span>content</span></div>`,
  3
)

渲染时直接通过 innerHTML 插入 3 个子节点,避免重复 vnode 构建。


五、扩展与性能分析

  • SSR 加速:字符串化节点可直接拼接 HTML,无需虚拟节点 diff。
  • Hydration 优化:客户端仅需匹配静态 DOM 节点,不再重建。
  • 构建层增强:结合模板预编译可进一步减少运行时代码体积。

六、潜在问题与限制

  1. 动态数据误判风险:若某绑定看似常量但实际运行期改变,会导致渲染错误。
  2. HTML 语义限制:表格标签和部分语义化容器(如 <p><div></div></p>)不适合字符串化。
  3. 调试困难:静态字符串块不易追踪原始模板位置。
  4. 安全评审要求evaluateConstant 虽受控,但仍需审查安全边界。

七、总结

stringifyStatic 是 Vue 编译器中的一个高层次性能优化机制,将可预测的静态结构转化为最小化的 HTML 片段,从而减少运行时 vnode 创建与 DOM 操作。
其本质是编译期静态求值与字符串拼接的融合,体现了 Vue 在编译优化方向上“以空间换时间”的策略。


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

Vue 编译器源码解析:忽略副作用标签的 NodeTransform 实现

一、概念

在 Vue 的模板编译阶段(@vue/compiler-dom 模块中),编译器会将模板 AST(抽象语法树)转换为可执行的渲染函数。
但在解析 DOM 模板时,某些标签(如 <script><style>)被认为具有副作用(side effect) ,编译器不应将它们编译为运行时渲染输出的一部分。

这段源码定义了一个 NodeTransform(节点转换器) ,用于在 AST 转换阶段忽略这些副作用标签。


二、原理解析

Vue 模板编译流程简化如下:

template -> parser -> AST -> transforms -> codegen -> render function

NodeTransform 就是 “transforms” 阶段的核心组件之一。它会遍历每个 AST 节点,对节点进行修改、删除或警告。

本段代码定义的 ignoreSideEffectTags 转换器作用如下:

  1. 检测当前节点是否为普通 HTML 元素(ElementTypes.ELEMENT)。
  2. 检查标签名是否是 'script''style'
  3. 如果是,则在开发环境中发出编译警告,并从 AST 树中移除该节点。

三、代码逐行讲解

import { ElementTypes, type NodeTransform, NodeTypes } from '@vue/compiler-core'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'
  • @vue/compiler-core 导入基本 AST 节点类型与转换接口。
  • NodeTransform 是一个函数类型,用于定义节点转换逻辑。
  • 从本地模块导入 DOMErrorCodescreateDOMCompilerError,用于创建 DOM 编译阶段的错误对象。

export const ignoreSideEffectTags: NodeTransform = (node, context) => {
  • 定义并导出一个名为 ignoreSideEffectTags 的节点转换函数。

  • 接受两个参数:

    • node:当前正在遍历的 AST 节点。
    • context:转换上下文,包含操作 AST 的工具方法。

  if (
    node.type === NodeTypes.ELEMENT &&
    node.tagType === ElementTypes.ELEMENT &&
    (node.tag === 'script' || node.tag === 'style')
  ) {
  • 逻辑判断部分:

    • node.type === NodeTypes.ELEMENT:确认该节点是一个元素节点。
    • node.tagType === ElementTypes.ELEMENT:确认它是普通的 HTML 元素(而非组件、插槽等)。
    • (node.tag === 'script' || node.tag === 'style'):匹配 <script><style>

    __DEV__ &&
      context.onError(
        createDOMCompilerError(
          DOMErrorCodes.X_IGNORED_SIDE_EFFECT_TAG,
          node.loc,
        ),
      )
  • 如果处于开发环境(__DEV__),通过 context.onError() 抛出编译警告。
  • createDOMCompilerError() 会创建一个带有位置信息 (node.loc) 的错误对象。
  • 错误码 X_IGNORED_SIDE_EFFECT_TAG 通常对应提示信息如:“忽略副作用标签 <script><style>”。

    context.removeNode()
  • 调用上下文的 removeNode() 方法,从 AST 树中删除该节点。
  • 这样在后续代码生成阶段,渲染函数中将不会包含这些标签。

  }
}
  • 结束判断与函数定义。整个逻辑非常简洁清晰:检测 → 报警 → 删除。

四、对比分析

场景 是否被保留 原因
<div><p> 等普通标签 可安全渲染,无副作用
<script> 可能执行外部 JS,引发安全或运行时问题
<style> 属于样式声明,不应出现在渲染输出
<component> / 动态组件 属于运行时逻辑,安全

相比 React 的 JSX 编译机制,Vue 的编译器在模板编译阶段进行安全过滤,避免后期运行时执行危险标签。


五、实践示例

示例模板

<div>Hello</div>
<style>.red { color: red; }</style>
<script>alert('hi')</script>

编译前后效果

  • 编译前 AST:包含 <div><style><script> 三个元素节点。
  • 经过 ignoreSideEffectTags 转换后
    仅保留 <div> 节点;<style><script> 被删除。

最终渲染结果:

<div>Hello</div>

六、拓展理解

1. 安全机制

此逻辑体现了 Vue 编译器的 防注入设计。如果模板源自用户输入,自动删除 <script> 可防止 XSS 攻击。

2. 可扩展性

开发者可自定义 NodeTransform,在编译阶段实现如:

  • 统计特定标签使用次数;
  • 自动替换标签;
  • 插入自定义指令。

七、潜在问题与注意事项

  1. 仅影响模板编译,不影响运行时插入的节点
    若通过 v-html 动态插入 <script>,仍需额外安全处理。
  2. 样式隔离问题
    被删除的 <style> 不会自动编译为 scoped 样式,应通过单文件组件机制处理。
  3. 错误信息本地化
    DOMErrorCodes.X_IGNORED_SIDE_EFFECT_TAG 的提示文字依赖内部错误表,可能需要在编译工具链中补充本地语言提示。

八、总结

这段源码在 Vue 编译器中承担了重要的“安全守门员”角色:
在模板编译阶段,主动识别并移除 <script><style> 标签,避免渲染层出现副作用或潜在安全风险。
其实现虽然简短,但在框架设计层面体现了 Vue 对模板安全与运行时隔离的严格要求。


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

Cursor 2.0 支持模型并发,我用国产 RWKV 模型实现了一模一样的效果 🤩🤩🤩

最近 Cursor 发布 2.0 版本,其中一个比较亮点的功能就是它可以同时指挥 8 个 Agent 执行任务,最后选择你觉得最好的那个答案。

而在底层模型层面,来自中国本土团队的 RWKV 项目也带来了更具突破性的成果:RWKV7-G0a3 13.3B ——当前全球最强的开源纯 RNN 大语言模型。

这一版本以 RWKV6-world-v2.1 14B 为基础,继续训练了 2 万亿 tokens(并融合了 35B 来自 DeepSeek v3.1 的高质量语料),在保持完全 RNN 架构、无注意力机制(No Attention)、无微调、无刷榜的前提下,取得了与主流 Transformer 模型相媲美甚至更优的表现。

20251104141030

在多项权威基准测试中(包括 MMLU、MMLU-Pro、GSM8K、MATH500、CEval 等),RWKV7-G0a3 在语言理解、逻辑推理与数学推演等任务上均实现显著提升。其中,MMLU-Pro 测评显示模型在多学科综合知识上的掌握更加扎实;GSM8K 与 MATH500 结果表明,其在中高难度数学与逻辑问题上的推理能力已达到同规模模型的领先水平。与此同时,RWKV7-G0a3 继续保持了 RWKV 系列一贯的高推理效率与低显存占用优势,展现出纯 RNN 架构在大模型时代下的强大潜力。

Uncheatable Eval 使用最新的论文、新闻、代码与小说等实时数据进行评测,通过“压缩率”(即 Compression is Intelligence)指标,衡量模型在真实语料下的语言建模能力与泛化水平。

20251104141244

MMLU 系列用于测评语言模型在多学科知识与认知推理方面的能力,其中 MMLU Pro 为进阶版本,包含更复杂的问题设计与更严苛的评测标准。

20251104141616

欲获取更多详细信息,请访问该模型的 官方公众号文章 阅读。

这意味着:

在以 Transformer (deep learning architecture) 架构主导的大模型时代,RWKV 所代表的“纯 RNN ”路线再度崛起:以更低的计算与显存成本、更自然的时序记忆机制,走出一条与主流 LLM 截然不同的进化路径。

RWKV 命名规则中,G0a3 标识了训练数据在版本与质量上的升级(例如:质量层级为 G# > G#a2 > G#,数据规模层级为 G1 > G0),即便参数量相同,G0a3 系列在泛化能力上也具备潜在优势。综合来看,RWKV7-G0a3 13.3B 的发布,不仅刷新了 RNN 模型性能的新高度,也象征着 RWKV 系列在“摆脱 Transformer 架构垄断”路径上迈出了一步。

模型下载

下载 RWKV7-G0a3 13.3B 模型(.pth 格式):

下载 .gguf 格式: modelscope.cn/models/shou…

下载 Ollama 格式: ollama.com/mollysama

如何使用 RWKV 模型(本地部署)

可以使用 RWKV RunnerAi00rwkv pip 等推理工具在本地部署 RWKV 模型。

RWKV 模型同时兼容主流推理框架,如 llama.cppOllama

目前最快的 RWKV 推理工具是 Albatross

由于 RWKV7-G0a3 13.3B 属于新模型,建议优先使用 RWKV Runner 以确保结果稳定与准确。

更多关于部署与推理的使用教程,可参考 RWKV 官网 - 模型部署和推理教程

前端如何实现模型并发的效果

首先,我们要知道模型并发的效果,那我们要知道连接了发起了一个请求之后,它是怎么回复的:

20251105181748

我们现在对的网络请求已经进行了截取,这是其中的一些数据,我们将一下核心的数据写到 json 文件里面让他能够更好的展示:

20251105181910

首先我们知道这是一个流式返回,但是一次流式返回了包含了的内容非常多,这里就是我们并发的关键了,这里的 index 代表并发的下标,而 delta.content 是具体的内容,这样我们知道了 SSE 实现并发的原理了,实际上就是调用 SSE,后端在一次 SSE 的返回中返回同一个问题的不同的结果并通过下标来区分。

我们已经把 SSE 返回机制摸清楚了。下面就轻松地走一遍“边生成边展示”的整个流程:从流式到达、到何时更新、再到怎么把半成品 HTML 安全地渲染出来,最后配上 UI 的滚动与分批加载。读完你就能一眼看懂这套实时渲染是怎么跑起来的。

先说结论:这件事其实就五步,顺次串起来就好了——流式接收、增量累积与触发、HTML 提取与补全、UI 局部更新、以及 iframe 的分批渲染。下面逐段拆开讲。

一、流式数据接收(ai.ts:195–251)

// 使用 ReadableStream 读取流式数据
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let partial = ""; // 处理不完整的行

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  const chunk = decoder.decode(value, { stream: true });
  partial += chunk;

  // 逐行解析 SSE 格式:data: {...}
  const lines = partial.split("\n");
  partial = lines.pop() || ""; // 保留不完整的行

  for (const line of lines) {
    if (!line.startsWith("data: ")) continue;
    const json: StreamChunk = JSON.parse(data);
    // 处理每个 chunk...
  }
}

这里的代码的核心要点如下:

  • 逐行处理:SSE 数据按行到达,split('\n') 拆开,半截 JSON 用 partial 暂存。
  • 字符安全:TextDecoder(..., { stream: true }) 负责拼接多字节字符,避免中文或 emoji 被截断。
  • 过滤噪声:只解析 data: 开头的有效行,忽略心跳、空行、注释。
  • 流式收尾:遇到 [DONE] 仅结束对应流 index,其余继续处理。

二、增量累积与智能触发(ai.ts:224–244)

// 为每个 index 累积内容
contentBuffers[index] += delta;

// 判断是否应该触发渲染
const lastLength = lastRenderedLength.get(index) || 0;
const shouldRender = this.shouldTriggerRender(
  contentBuffers[index],
  lastLength
);

if (shouldRender && onProgress) {
  const htmlCode = this.extractHTMLCode(contentBuffers[index]);
  onProgress(index, contentBuffers[index], htmlCode);
  lastRenderedLength.set(index, contentBuffers[index].length);
}

这里的代码的核心要点如下:

  • 多流并行:每个 index 各自独立累积,互不干扰。
  • 智能触发:通过与 lastRenderedLength 比较控制频率,避免“来一点就刷”。
  • 精准更新:只触发对应 index 的渲染,避免全局重排。
  • 兜底刷新:流结束后进行最终更新,确保结果完整。

三、触发策略(ai.ts:62–101)

private static shouldTriggerRender(
  newContent: string,
  oldLength: number,
): boolean {
  // 1. 首次渲染:内容超过 20 字符
  if (oldLength === 0 && newLength > 20) {
    return true;
  }

  // 2. 关键闭合标签出现(语义区块完成)
  const keyClosingTags = [
    '</header>', '</section>', '</main>',
    '</article>', '</footer>', '</nav>',
    '</aside>', '</div>', '</body>', '</html>'
  ];

  const addedContent = newContent.substring(oldLength);
  for (const tag of keyClosingTags) {
    if (addedContent.includes(tag)) {
      return true; // 区块完成,立即渲染
    }
  }

  // 3. 内容增长超过 200 字符(防止长时间不更新)
  if (newLength - oldLength > 200) {
    return true;
  }

  return false;
}

这里的代码的核心要点如下:

  • 首帧提速:内容首次超过 20 字符立即渲染,减少“首屏空白”。
  • 语义闭合优先:检测新增片段中的关键闭合标签(如 </section></div>),保证块级内容完整展示。
  • 超长兜底:即使未闭合,增量超 200 字符也强制刷新。
  • 性能友好:仅比较“新增部分”,无需重复扫描旧文本;参数可根据模型节奏与设备性能调节。

四、HTML 提取与自动补全(ai.ts:19–59, 104–145)

private static extractHTMLCode(content: string): string {
  // 方式1: 完整的 ```html 代码块
  const codeBlockMatch = content.match(/```html\s*([\s\S]*?)```/);
  if (codeBlockMatch) return codeBlockMatch[1].trim();

  // 方式2: 未完成的代码块(流式渲染)
  const incompleteMatch = content.match(/```html\s*([\s\S]*?)$/);
  if (incompleteMatch) {
    return this.autoCompleteHTML(incompleteMatch[1].trim());
  }

  // 方式3: 直接以 <!DOCTYPE 或 <html 开头
  if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html')) {
    return this.autoCompleteHTML(trimmed);
  }

  return '';
}

private static autoCompleteHTML(html: string): string {
  // 移除最后不完整的标签(如 "<div cla")
  if (lastOpenBracket > lastCloseBracket) {
    result = html.substring(0, lastOpenBracket);
  }

  // 自动闭合 script、body、html 标签
  // 确保浏览器可以渲染未完成的 HTML
  return result;
}

这里的代码的核心要点如下:

  • 多格式兼容:完整块、未闭合块、裸 HTML 均可识别。
  • 容错补齐:遇到半截标签(如 <div cla)自动裁剪,再补上 </script></body></html> 等关键闭合。
  • 最小修正:仅做“可渲染”层面的修复,保持生成内容原貌。
  • 安全回退:提不出 HTML 时返回空字符串,避免将解释性文字误渲染。

五、UI 实时更新(ChatPage.tsx:240–253)

await AIService.generateMultipleResponses(
  userPrompt,
  totalCount,
  (index, content, htmlCode) => {
    // 实时更新对应 index 的结果
    setResults((prev) =>
      prev.map((result, i) =>
        i === index
          ? {
              ...result,
              content, // 原始 Markdown 内容
              htmlCode, // 提取的 HTML 代码
              isLoading: false,
            }
          : result
      )
    );
  }
);

这里的代码的核心要点如下:

  • 局部更新:仅更新目标项,prev.map 保证不可变数据结构,减少重渲染。
  • 双轨推进:content 用于 Markdown 文本,htmlCode 用于预览展示。
  • 快速反馈:首批数据到达即撤骨架屏,让用户感知“正在生成”。
  • 状态持久:结果存入 sessionStorage,刷新或返回依旧保留上下文。

六、iframe 分批渲染优化(ChatPage.tsx:109–175)

// 找出已准备好但还未渲染的索引
const readyIndexes = results
  .filter(({ result }) => !result.isLoading && result.htmlCode)
  .map(({ index }) => index)
  .sort((a, b) => a - b);

// 第一次渲染:一次性全部加载(用户体验优先)
if (!hasRenderedOnce.current) {
  setIframeRenderQueue(new Set(readyIndexes));
  hasRenderedOnce.current = true;
  return;
}

// 后续渲染:分批加载(每批 8 个,间隔 300ms)
// 避免一次性创建太多 iframe 导致卡顿
const processBatch = () => {
  const toAdd = stillNotInQueue.slice(0, batchSize); // 8 个
  toAdd.forEach((index) => newQueue.add(index));

  if (stillNotInQueue.length > batchSize) {
    setTimeout(processBatch, 300); // 继续下一批
  }
};

这里的代码的核心要点如下:

  • 首批全放:初次渲染不延迟,保证响应速度。
  • 后续分批:按批次(默认 8 个/300ms)渐进挂载,防止主线程卡顿。
  • 动态调度:每轮重新计算“未入队项”,保证不遗漏。
  • 轻量 DOM:仅渲染必要 iframe,滚动与交互更顺滑;参数可按性能灵活调整。

小结

通过语义闭合与字数阈值控制更新频率让画面稳定流畅,HTML 半成品自动补齐避免黑屏,iframe 分批挂载减轻主线程压力并配合 requestAnimationFrame 提升滚动顺滑度,状态由 sessionStorage 兜底并以日志辅助调参;整体逻辑是流式接收边累积、攒到关键点就渲染一帧、UI 精准更新该动的那格,调顺节奏即可实现实时渲染的又快又稳。

效果展示

前面我们说了这么多代码相关的,接下来我们可以把我们的项目运行起来看一下最终运行的效果:

20251105190158

为了让 UI 的效果显示得更好,建议使用 33%缩放的屏幕效果。

20251105190242

在输入框输入我们要问的问题,点击发送,你会看到这样的效果:

20251105190307

这会你能实时看到 24 个页面实时渲染的效果:

20251105190430

这样我们就借助 RWKV7-G0a3 13.3B 模型实现了一个跟 Cursor2.0 版本一模一样的效果了。

总结

RWKV7-G0a3 13.3B 是由中国团队推出的最新一代纯 RNN 大语言模型,在无 Attention 架构下实现了与主流 Transformer 模型相媲美的性能,并在多项基准测试中表现优异。它以更低显存占用和高推理效率展示了 RNN 架构的强大潜力。而前端并发实现中,通过 SSE 流式返回不同 index 的内容,实现了同时生成多个模型响应的并行效果。结合智能触发渲染与分批 iframe 更新,最终达成了类似 Cursor 2.0 的多 Agent 实时对比体验。

前端仓库地址

后端仓库地址

基于Monaco的diffEditor实现内容对比

前言

最近收到一个需求,实现两个配置文件对比的能。一开始想着那简单直接用采用monacodiffEditor组件就可以了。在开发的时候发现没这么简单,因为monaco内置的diffEditor只有两种状态新增删除,但是我们产品需要我们存在三种状态新增删除更新

  • monaco默认的效果,行样式没有与我的样式保持一致,只存在两种状态 image.png

  • 我需要实现效果,行样式保持一致,并且存在三种状态 image.png

需求分析

  1. 需要计算出新增删除差异各占多少行,这里采用diffEidtor提供的getLineChanges方法获取所有行改动,然后分析数据
  2. 如何判断新增行删除行差异行呢?(这里主要想明白,你的状态是跟着视图走的,左侧空行代表新增,右侧空行代表删除、两侧都存在代表更新,是不是一说就明白呢? 但是我之前还结合charChanges算了好久,后面发现根本就不需要)
1. 因为originalStartLineNumber和originalEndLineNumber为1,而modifiedStartLineNumber和modifiedEndLineNumber是1-2。那么表示第一行为更新状态、第二行为新增状态
2. 由于originalStartLineNumber和originalEndLineNumber为3,但是modifiedEndLineNumber为0,那么表示更新后被移除了,则第三行为删除状态
[    {        "originalStartLineNumber": 1,        "originalEndLineNumber": 1,        "modifiedStartLineNumber": 1,        "modifiedEndLineNumber": 2,        "charChanges": [...]
    },
    {
        "originalStartLineNumber": 3,
        "originalEndLineNumber": 3,
        "modifiedStartLineNumber": 3,
        "modifiedEndLineNumber": 0
    }
]

3. 想明白新增行删除行差异行的计算,那么我们就聚焦到这些行变化的颜色,其实也不算复杂,首先将默认行的背景色改为透明、然后我们再根据变更状态添加对应的行装饰器就可以实现我们需要的效果了

代码实现

  1. 设置diffEditor变化的背景色为透明
    // 覆盖Monaco Editor的默认diff样式
    .monaco-diff-editor .line-insert {
      background-color: transparent !important;
    }

    .monaco-diff-editor .line-delete {
      background-color: transparent !important;
    }

    .monaco-editor .line-insert {
      background-color: transparent !important;
    }

    .monaco-editor .line-delete {
      background-color: transparent !important;
    }

    // 将整行的char-delete和line-delete背景设为透明,但保留字符级别的删除标记
    .monaco-diff-editor .char-delete[style*='width:100%'] {
      background-color: transparent !important;
    }

    .monaco-diff-editor .char-insert[style*='width:100%'] {
      background-color: transparent !important;
    }

    // 简单的diff行样式 - 参考断点行的实现方式
    .diff-line-added {
      background-color: #44ca6240 !important;
    }

    .diff-line-deleted {
      background-color: #f87d7c40 !important;
    }

    .diff-line-modified {
      background-color: #ffad5d40 !important;
    }

    // 暗色主题
    .monaco-editor.vs-dark {
      // 覆盖暗色主题下的Monaco默认样式
      .line-insert {
        background-color: transparent !important;
      }

      .line-delete {
        background-color: transparent !important;
      }

      .diff-line-added {
        background-color: #44ca6260 !important;
      }

      .diff-line-deleted {
        background-color: #f87d7c60 !important;
      }

      .diff-line-modified {
        background-color: #ffad5d60 !important;
      }

      .char-delete[style*='width:100%'] {
        background-color: transparent !important;
      }

      .char-insert[style*='width:100%'] {
        background-color: transparent !important;
      }
    }
  1. 注册DiffEditor编辑器,主要关注的是onMount的处理
   <DiffEditor
    width="900"
    height="300"
    language="javascript"
    theme={
      this.props.colorMode === ColorMode.Light
        ? 'vs-light'
        : 'vs-dark'
    }
    original={leftTest}
    modified={rightTest}
    options={options}
    onMount={this.editorDidMount}
    {...config}
  />
  1. 当编辑器加载完成时,onDidUpdateDiff监听文本变化,然后执行applyCustomDiffDecorations
  editorDidMount(editor, monaco) {
    this.diffEditor = editor
    this.monaco = monaco

    // 调用 onRef 回调,将当前组件实例传递给父组件
    this.onRef(this)

    // 防抖函数,避免频繁调用
    let debounceTimer = null

    // 监听差异更新事件
    editor.onDidUpdateDiff(() => {
      // 清除之前的定时器
      if (debounceTimer) {
        clearTimeout(debounceTimer)
      }

      // 设置新的定时器,延迟执行
      debounceTimer = setTimeout(() => {
        this.applyCustomDiffDecorations()
      }, 100) // 100ms 防抖
    })
  }
  1. 基于monaco的[deltaDecorations]实现行装饰器,stats就是新增删除差异的数据统计
// 应用自定义diff装饰并计算差异统计
  applyCustomDiffDecorations() {
    if (!this.diffEditor || !this.monaco) return
    const lineChanges = this.diffEditor.getLineChanges()

    if (!lineChanges || lineChanges.length === 0) {
      // 清除之前的装饰
      if (this.originalDecorationIds) {
        this.diffEditor
          .getOriginalEditor()
          .deltaDecorations(this.originalDecorationIds, [])
      }
      if (this.modifiedDecorationIds) {
        this.diffEditor
          .getModifiedEditor()
          .deltaDecorations(this.modifiedDecorationIds, [])
      }

      // 重置差异统计
      this.updateDiffStatsIfChanged({
        additions: 0,
        deletions: 0,
        modifications: 0,
      })
      return
    }

    const originalEditor = this.diffEditor.getOriginalEditor()
    const modifiedEditor = this.diffEditor.getModifiedEditor()

    const originalDecorations = []
    const modifiedDecorations = []

    // 差异统计
    const stats = {
      additions: 0,
      deletions: 0,
      modifications: 0,
    }

    // 使用Map来记录每一行的变更类型,避免重复处理
    const allOriginalLineTypes = new Map() // 左侧编辑器行类型
    const allModifiedLineTypes = new Map() // 右侧编辑器行类型

    lineChanges.forEach((change) => {
      const originalStartLine = change.originalStartLineNumber
      const originalEndLine = change.originalEndLineNumber
      const modifiedStartLine = change.modifiedStartLineNumber
      const modifiedEndLine = change.modifiedEndLineNumber

      // 当前变更的行类型
      const originalLineTypes = new Map() // 左侧编辑器行类型
      const modifiedLineTypes = new Map() // 右侧编辑器行类型

      // 根据用户提供的规则进行判断
      if (originalEndLine === 0 && modifiedEndLine > 0) {
        for (let i = modifiedStartLine; i <= modifiedEndLine; i++) {
          modifiedLineTypes.set(i, 'added')
        }
      } else if (originalEndLine > 0 && modifiedEndLine === 0) {
        for (let i = originalStartLine; i <= originalEndLine; i++) {
          originalLineTypes.set(i, 'deleted')
        }
      } else if (originalEndLine > 0 && modifiedEndLine > 0) {
        // 规则3: 两边都有行号,需要根据行数差异判断
        const originalLines = originalEndLine - originalStartLine + 1
        const modifiedLines = modifiedEndLine - modifiedStartLine + 1

        if (originalLines === modifiedLines) {
          // 行数相同,全部标记为修改
          for (let i = originalStartLine; i <= originalEndLine; i++) {
            originalLineTypes.set(i, 'modified')
          }
          for (let i = modifiedStartLine; i <= modifiedEndLine; i++) {
            modifiedLineTypes.set(i, 'modified')
          }
        } else {
          // 行数不同,按照用户规则处理
          const minLines = Math.min(originalLines, modifiedLines)

          if (originalLines > modifiedLines) {
            // 左侧行数更多:对应行标记为修改,多出的左侧行标记为删除
            for (let i = 0; i < minLines; i++) {
              originalLineTypes.set(originalStartLine + i, 'modified')
              modifiedLineTypes.set(modifiedStartLine + i, 'modified')
            }
            // 多出的左侧行标记为删除
            for (let i = minLines; i < originalLines; i++) {
              originalLineTypes.set(originalStartLine + i, 'deleted')
            }
          } else {
            for (let i = 0; i < minLines; i++) {
              originalLineTypes.set(originalStartLine + i, 'modified')
              modifiedLineTypes.set(modifiedStartLine + i, 'modified')
            }
            // 多出的右侧行标记为新增
            for (let i = minLines; i < modifiedLines; i++) {
              modifiedLineTypes.set(modifiedStartLine + i, 'added')
            }
          }
        }
      }

      // 统计各类型行数
      const addedCount = Array.from(modifiedLineTypes.values()).filter(
        (type) => type === 'added',
      ).length
      const deletedCount = Array.from(originalLineTypes.values()).filter(
        (type) => type === 'deleted',
      ).length
      const modifiedCount = Math.max(
        Array.from(originalLineTypes.values()).filter(
          (type) => type === 'modified',
        ).length,
        Array.from(modifiedLineTypes.values()).filter(
          (type) => type === 'modified',
        ).length,
      )

      stats.additions += addedCount
      stats.deletions += deletedCount
      stats.modifications += modifiedCount

      // 将当前变更的行类型合并到全局Map中
      originalLineTypes.forEach((type, lineNumber) => {
        allOriginalLineTypes.set(lineNumber, type)
      })
      modifiedLineTypes.forEach((type, lineNumber) => {
        allModifiedLineTypes.set(lineNumber, type)
      })

      // 根据行类型添加装饰器
      // 处理左侧编辑器
      originalLineTypes.forEach((type, lineNumber) => {
        if (type === 'deleted') {
          // 删除行 - 添加红色背景装饰
          originalDecorations.push({
            range: new this.monaco.Range(lineNumber, 1, lineNumber, 1),
            options: {
              isWholeLine: true,
              className: 'diff-line-deleted',
            },
          })
        } else if (type === 'modified') {
          // 修改行 - 添加橙色背景
          originalDecorations.push({
            range: new this.monaco.Range(lineNumber, 1, lineNumber, 1),
            options: {
              isWholeLine: true,
              className: 'diff-line-modified',
            },
          })
        }
      })

      // 处理右侧编辑器
      modifiedLineTypes.forEach((type, lineNumber) => {
        if (type === 'added') {
          // 新增行 - 添加绿色背景装饰
          modifiedDecorations.push({
            range: new this.monaco.Range(lineNumber, 1, lineNumber, 1),
            options: {
              isWholeLine: true,
              className: 'diff-line-added',
            },
          })
        } else if (type === 'modified') {
          // 修改行 - 添加橙色背景
          modifiedDecorations.push({
            range: new this.monaco.Range(lineNumber, 1, lineNumber, 1),
            options: {
              isWholeLine: true,
              className: 'diff-line-modified',
            },
          })
        }
      })
    })

    // 更新差异统计
    this.updateDiffStatsIfChanged(stats)

    // 应用装饰并保存装饰ID以便后续清理
    this.originalDecorationIds = originalEditor.deltaDecorations(
      this.originalDecorationIds || [],
      originalDecorations,
    )
    this.modifiedDecorationIds = modifiedEditor.deltaDecorations(
      this.modifiedDecorationIds || [],
      modifiedDecorations,
    )
  }

总结

这一节主要讲解了monaco的DiffEditor实现配置文件对比。在这一章我们也初步学习了Monaco的行装饰器的使用,其实编辑器的debugger模式,先基于DAP协议获取到当前debugger的堆栈聚焦行,然后我们在通过行装饰器绘制对应的高亮行。至于堆栈信息只需要绘制对应的堆栈面板接口,是不是感觉就特别清晰了

为什么写这篇文章呢?

  1. 是因为我没有找到相关文章,其他文章都是直接实现DiffEditor效果,并不满足需要的三种状态新增删除差异
  2. 在研发任务排期紧张的时候帮助遇到相同需求的小伙伴减少工作压力,哈哈哈。
❌