← 返回文章列表
Vue·1714 次阅读

Vue 2 插槽进阶:作用域插槽的正确打开方式

年前做后台系统,有个需求:同一种数据列表,A 页面要显示成表格,B 页面要显示成卡片。我本来想传个 mode 字段在组件里 if-else,写到一半意识到——这正是作用域插槽的教科书场景。

插槽的基础用法(具名 slot)就不赘述了,这篇直接上硬核部分。

问题的本质

普通插槽有个限制:插槽内容在父组件作用域编译,访问不到子组件的数据。

<!-- 父组件模板 -->
<user-card>
  <!-- ❌ 报错:user 是子组件里的数据,这里访问不到 -->
  <p>{{ user.name }}</p>
</user-card>

但"数据在子组件、渲染结构由父组件定"这个需求太常见了——所有"列表/表格/树"类组件都长这样。作用域插槽就是用来打通这层数据通道的。

语法拆解

分两步:子组件把数据传出去,父组件接住数据再渲染。

子组件 UserList.vue:

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <!-- 把 user 作为插槽的 props 传出去 -->
      <slot :user="user" :index="$index">
        <!-- 兜底内容:父组件没给插槽时显示 -->
        {{ user.name }}
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      users: [
        { id: 1, name: '张三', role: '管理员' },
        { id: 2, name: '李四', role: '运营' },
      ],
    }
  },
}
</script>

父组件接住:

<user-list>
  <!-- v-slot 声明一个插槽,slotProps 接收子组件传出的所有数据 -->
  <template v-slot:default="slotProps">
    <strong>{{ slotProps.user.name }}</strong>
    <em>({{ slotProps.user.role }})</em>
  </template>
</user-list>

实际项目里大家都会解构 + 用缩写 #:

<user-list>
  <template #default="{ user, index }">
    {{ index + 1 }}. {{ user.name }} —— {{ user.role }}
  </template>
</user-list>

核心就一句:子组件在 <slot> 上绑属性传数据,父组件用 v-slot 接数据。方向和普通 props 恰好相反,所以也有人叫它"反向传参"。

实战:一个通用的数据列表组件

回到开头的需求。做一个 DataList 组件,逻辑(数据获取、加载态、空态)全包,渲染结构完全交给使用方:

<template>
  <div class="data-list">
    <div v-if="loading" class="state">加载中...</div>
    <div v-else-if="!list.length" class="state">暂无数据</div>
    <template v-else>
      <div v-for="item in list" :key="item.id" class="data-item">
        <slot :item="item" />
      </div>
    </template>
  </div>
</template>

<script>
export default {
  props: {
    fetcher: { type: Function, required: true },
  },
  data() {
    return { list: [], loading: true }
  },
  async created() {
    this.list = await this.fetcher()
    this.loading = false
  },
}
</script>

A 页面用表格风格渲染:

<data-list :fetcher="fetchUsers">
  <template #default="{ item }">
    <div class="row">
      <span>{{ item.name }}</span>
      <span>{{ item.role }}</span>
    </div>
  </template>
</data-list>

B 页面用卡片风格渲染:

<data-list :fetcher="fetchUsers">
  <template #default="{ item }">
    <div class="card">
      <h3>{{ item.name }}</h3>
      <p>{{ item.role }}</p>
    </div>
  </template>
</data-list>

数据逻辑零重复,展示层完全自由。这就是 Element UI 的 Table 列、iView 的 List 都在用的模式。

两个容易绕晕的点

1. 解构重命名 + 默认值

<template #default="{ item: row = {} }">
  {{ row.name || '未知' }}
</template>

插槽 props 的解构和函数参数解构规则一致,可以重命名、给默认值。

2. 作用域插槽的性能细节

Vue 2 里作用域插槽的渲染和普通插槽有差异:普通插槽在父组件更新时重渲染,作用域插槽作为一个函数,在子组件更新时才会重新执行。2.6 版本引入 v-slot 语法时做过一轮优化,尽量保证它只依赖传入的数据。如果你在列表里发现插槽内容没更新,先检查是不是依赖了插槽作用域之外的数据。

小结

  • 作用域插槽解决"数据在子、结构在父"的矛盾,本质是子组件向插槽反向传参
  • 子组件 <slot :user="user">,父组件 #default="{ user }"
  • 列表/表格/树类组件的标准设计:组件管数据和状态,插槽管渲染
  • 记准方向:普通 props 父→子,插槽 props 子→父

理解了作用域插槽,才算真正摸到了 Vue 组件复用的精髓——逻辑和表现的彻底分离。

看到这里了?如果这篇文章帮到过你

请我喝瓶可乐就好 🥤

收款码

评论0 条

还没有评论,来抢沙发~

发表评论

评论需审核后显示

© 2026 许冬博客 · 专注 JS 技术分享

·RSS 订阅·Powered by Nuxt 4