Vue SFC 编译器源码解析:processPropsDestructure 与 transformDestructuredProps

一、概念

在 Vue 3 的 <script setup> 编译阶段,defineProps() 支持结构赋值语法:

typescript 复制代码
const { foo, bar } = defineProps<{ foo: string; bar: number }>()

编译器需要在转换过程中:

  1. 注册结构后的变量(foo, bar);
  2. 保留其与原始 prop 名的映射;
  3. 禁止对这些变量的重新赋值;
  4. 保证在运行时能正确访问到组件的 props。

这一过程正是由 processPropsDestructuretransformDestructuredProps 两个函数配合完成的。


二、原理

这两段逻辑主要位于 @vue/compiler-sfc 内部的 script/compileScript.ts 模块。

它们的职责如下:

  • processPropsDestructure :分析 defineProps() 的解构模式,建立绑定元数据;
  • transformDestructuredProps :遍历整个 AST,检测并替换结构出来的变量为对 __props 的访问。

整体编译思路是「先收集,再替换」:

scss 复制代码
parse AST
 └──> detect defineProps destructuring
       └──> register bindings (processPropsDestructure)
             └──> later transform identifier usages (transformDestructuredProps)

三、源码分节详解

1. processPropsDestructure

javascript 复制代码
export function processPropsDestructure(
  ctx: ScriptCompileContext,
  declId: ObjectPattern,
): void {
  if (ctx.options.propsDestructure === 'error') {
    ctx.error(`Props destructure is explicitly prohibited via config.`, declId)
  } else if (ctx.options.propsDestructure === false) {
    return
  }

  ctx.propsDestructureDecl = declId

🔍 注释解析:

  • 如果配置禁止解构,则直接报错或跳过;
  • 将解构节点 (ObjectPattern) 存入上下文,方便后续引用。

接下来定义一个内部注册函数 registerBinding

vbnet 复制代码
const registerBinding = (
  key: string,
  local: string,
  defaultValue?: Expression,
) => {
  ctx.propsDestructuredBindings[key] = { local, default: defaultValue }
  if (local !== key) {
    ctx.bindingMetadata[local] = BindingTypes.PROPS_ALIASED
    ;(ctx.bindingMetadata.__propsAliases ||
      (ctx.bindingMetadata.__propsAliases = {}))[local] = key
  }
}

💡 功能说明:

  • key:原始 prop 名;
  • local:解构后的本地变量名;
  • defaultValue:是否提供默认值。

例如:

css 复制代码
const { foo: localFoo = 123 } = defineProps<{ foo: number }>()

会得到:

css 复制代码
ctx.propsDestructuredBindings = {
  foo: { local: 'localFoo', default: 123 }
}

然后开始遍历结构属性:

rust 复制代码
for (const prop of declId.properties) {
  if (prop.type === 'ObjectProperty') {
    const propKey = resolveObjectKey(prop.key, prop.computed)
    ...
    if (prop.value.type === 'AssignmentPattern') {
      // 带默认值的结构
      registerBinding(propKey, left.name, right)
    } else if (prop.value.type === 'Identifier') {
      // 普通结构
      registerBinding(propKey, prop.value.name)
    }
  } else {
    // 处理剩余参数:const { foo, ...rest } = defineProps()
    ctx.propsDestructureRestId = (prop.argument as Identifier).name
  }
}

🧠 机制要点:

  • 禁止嵌套结构;
  • 禁止动态键名;
  • 自动登记 rest 变量为「响应式常量」。

2. transformDestructuredProps

这个函数负责第二阶段:AST 重写

其目标是:

  • 找出所有使用了解构 prop 的地方;
  • 将其替换为 __props.xxx
  • 防止直接修改这些变量;
  • 检测非法用法(例如直接传入 toRef(foo))。

(1)作用域与注册逻辑

typescript 复制代码
const rootScope: Scope = Object.create(null)
const scopeStack: Scope[] = [rootScope]
let currentScope: Scope = rootScope
const excludedIds = new WeakSet<Identifier>()
const propsLocalToPublicMap: Record<string, string> = Object.create(null)

💬 每个作用域(Scope)是一个对象:

yaml 复制代码
{ foo: true } // true 表示这是一个 prop 绑定
{ bar: false } // false 表示普通局部变量

(2)递归扫描与注册变量

ini 复制代码
function walkScope(node: Program | BlockStatement, isRoot = false) {
  for (const stmt of node.body) {
    if (stmt.type === 'VariableDeclaration') {
      walkVariableDeclaration(stmt, isRoot)
    } else if (stmt.type === 'FunctionDeclaration') {
      registerLocalBinding(stmt.id)
    }
    ...
  }
}

每次进入一个新的函数或块作用域时:

scss 复制代码
pushScope()
walkScope(node.body)
popScope()

这样可以保证在不同层级区分同名变量,防止误替换。


(3)重写标识符

核心逻辑在 rewriteId

python 复制代码
function rewriteId(id: Identifier, parent: Node, parentStack: Node[]) {
  if (
    (parent.type === 'AssignmentExpression' && id === parent.left) ||
    parent.type === 'UpdateExpression'
  ) {
    ctx.error(`Cannot assign to destructured props as they are readonly.`, id)
  }

  if (isStaticProperty(parent) && parent.shorthand) {
    ctx.s.appendLeft(
      id.end! + ctx.startOffset!,
      `: ${genPropsAccessExp(propsLocalToPublicMap[id.name])}`,
    )
  } else {
    ctx.s.overwrite(
      id.start! + ctx.startOffset!,
      id.end! + ctx.startOffset!,
      genPropsAccessExp(propsLocalToPublicMap[id.name]),
    )
  }
}

🧩 示例:

arduino 复制代码
console.log(foo)

会被改写为:

arduino 复制代码
console.log(__props.foo)

(4)非法用法检测

lua 复制代码
function checkUsage(node: Node, method: string, alias = method) {
  if (isCallOf(node, alias)) {
    const arg = unwrapTSNode(node.arguments[0])
    if (arg.type === 'Identifier' && currentScope[arg.name]) {
      ctx.error(
        `"${arg.name}" is a destructured prop and should not be passed directly to ${method}(). Pass a getter () => ${arg.name} instead.`,
        arg,
      )
    }
  }
}

🧠 用法限制:

scss 复制代码
watch(foo) ❌ // 错误
watch(() => foo) ✅

四、对比

功能点 processPropsDestructure transformDestructuredProps
阶段 AST 收集阶段 AST 遍历 & 转换阶段
输入 解构声明 (ObjectPattern) 整个脚本 AST
产出 props → local 映射表 重写后的代码、错误提示
作用 记录关系 替换标识符引用

五、实践:编译前后对比

输入:

typescript 复制代码
const { foo, bar } = defineProps<{ foo: string; bar: number }>()
console.log(foo, bar)

输出(编译后):

ini 复制代码
const __props = defineProps();
console.log(__props.foo, __props.bar);

六、拓展思考

  • 为什么要禁止赋值?

    因为 props 是只读的,Vue 内部通过 Proxy 实现响应式引用,重新赋值会破坏单向数据流。

  • 为什么需要作用域栈?

    为了区分局部变量与外层 prop 名称,例如:

    javascript 复制代码
    function test(foo) { console.log(foo) }

    这里的 foo 是局部变量,不应被替换为 __props.foo

  • 为何需要检测 watchtoRef

    它们接收的是响应式引用对象,而非普通值。直接传入解构变量无法追踪依赖变化。


七、潜在问题与改进

  1. 性能问题walk 整个 AST 对大型文件代价较高。
  2. 可扩展性:目前只支持一层解构,若未来 Vue 支持深层结构则需调整。
  3. 代码生成位置偏移 :由于 s.overwrite 基于字符偏移操作,AST 与源码偏移不匹配可能导致编译错误。

八、总结

processPropsDestructuretransformDestructuredProps 是 Vue <script setup> 编译的关键一环。

它们的设计保证了开发者能以最自然的语法使用 props 解构,同时不破坏响应式系统与只读约束。

🧩 一句话总结

Vue 编译器通过「静态分析 + 精准重写」,实现了安全、透明且直观的 props 解构支持。


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

相关推荐
excel2 小时前
深度解析 processDefineSlots:Vue SFC 编译阶段的 defineSlots 处理逻辑
前端
excel2 小时前
Vue SFC 模板依赖解析机制源码详解
前端
wfsm2 小时前
flowable使用01
java·前端·servlet
excel2 小时前
深度解析:Vue <script setup> 中的 defineModel 处理逻辑源码剖析
前端
excel2 小时前
🧩 深入理解 Vue 宏编译:processDefineOptions() 源码解析
前端
excel2 小时前
Vue 宏编译源码深度解析:processDefineProps 全流程解读
前端
excel2 小时前
Vue SFC 编译器源码深度解析:processDefineEmits 与运行时事件生成机制
前端
excel2 小时前
Vue 3 深度解析:defineModel() 与 defineProps() 的区别与底层机制
前端
excel2 小时前
深入解析 processDefineExpose:Vue SFC 编译阶段的辅助函数
前端