714 字
4 分钟

编译期宏和作用域插槽

欢迎来到我的博客!#

使用 Markdown 格式编写。

今天学到了什么#

  • 编译期宏
  • 作用域插槽

编译期宏#

<script setup> 里有两类东西:

  1. 普通 JS 运行时代码:比如 console.log(...)useAttrs() 的调用(它会拿到一个运行时的代理对象)。
  2. 编译期宏(compile-time macro)defineProps() / defineEmits() / defineExpose() 这类。它们看起来像函数调用,但实际上在构建/编译 SFC 时就被“抽走”了,用来生成组件的 props/emits 配置;并不是等到运行时才“执行”这行再决定分流。

所以你以为的线性过程是:

  • useAttrs() → attrs 里有 title
  • defineProps(['title']) → “改掉”attrs,让 title 消失

但真实过程更接近:

  • 编译阶段:编译器先扫描整个 <script setup>,看到你声明了 props: { title: ... }(由 defineProps 得到)。
  • 运行阶段(组件实例创建/更新):Vue 拿到父组件传入的所有绑定后,按“已声明为 props/emits 的归类规则”一次性分流:
    • title 既然是 prop → 进 props
    • 其余未声明的 → 进 attrs
  • useAttrs() 只是给你一个指向当前实例 attrs响应式只读代理,它反映的是“分流后的结果”。

另外还有个很常见的“误会来源”:

  • console.log(attrs) 打印的是一个代理对象,很多时候控制台是延迟展开/显示的;你看到的可能是“后来展开时的最新状态”,不一定是 log 那一刻的快照。
    • 想看“当时那一刻”的值,用:console.log({ ...attrs })console.log(JSON.parse(JSON.stringify(attrs)))(后者不推荐常用,只是为了观察)。

如果你愿意,我可以直接看你当前的组件结构(父组件怎么传 title、子组件怎么接),然后给你改成一个最小可观测例子:用 watchEffect 同时输出 props.title{...attrs},你会直观看到分流规则在起作用。

作用域插槽#

不是为了“绕一圈”,而是为了把两件事拆开:数据归谁管,页面长什么样归谁管。

作用域插槽的本质是:子组件把自己手里的上下文数据暴露出来,父组件来决定怎么渲染。看起来像“传回父组件”,其实不是把数据回传,而是父组件写的模板,拿到了子组件提供的参数,在子组件的位置被渲染出来。

所以要分情况看:

  • 如果数据本来就在父组件,而且父组件自己也能直接渲染,那就没必要再传给子组件再绕回来。
  • 如果子组件负责列表、状态、加载逻辑,父组件负责外观,那作用域插槽就很合适。
  • 它解决的是“子组件掌控数据,父组件掌控结构”的组合问题,不是为了增加传递层级。

赞助支持

如果这篇文章对你有帮助,欢迎赞助支持!

赞助
最后更新于 2026-04-21,距今已过 133 天

部分内容可能已过时

目录