vue2插槽slot、slot-scope和v-slot

5月底看vue2的时候不太明白插槽,最近好像有点懂了,赶紧缕一缕

v-slot 指令(统一 slot 和 scope-slot 语法)

  • v-slot指令自 Vue 2.6.0 起被引入,提供更好的支持 slotslot-scope 的 API 替代方案。slot 和 slot-scope被废除,改为用v-slot代替
  • v-slot 指令是被废除的 slotscope-slot 语法的统一,可以通过搞懂 slotscope-slot 来理解 v-slot
  • 使用总结: (可看下方“完整例子”)
    • 子组件中定义(具名)插槽,可绑定想要传给父组件使用插槽位置的变量
    • 父组件中使用(具名)插槽,可获取并使用子组件中定义插槽时绑定的变量

v-slot简写#

  • v-onv-bind 一样,v-slot 也有缩写,即**把 v-slot: 替换为字符 #**。
  • 例如: v-slot:header 可以被重写为 #header,和其它指令一样,该缩写只在其有参数的时候才可用 (具体可看下方“完整例子”)
  • 注意:使用缩写时,default不可省略,比如:
    1
    2
    3
    <Child #default="{ childData }">
    {{ childData.Name }}
    </Child>

插槽props

1
2
3
<!-- 子组件,给插槽绑定ab对象和arr数组 -->
<!-- arr: [0, 1], ab: { a:1, b:2 }, -->
<slot name="header" :a="ab.a" :b="ab.b" :ab="ab" :arr="arr"></slot>
1
2
3
4
5
6
7
8
9
<!-- 父组件 -->
<Child>
<template #header="slotProps">
<!-- 点击将改变子组件中arr -->
<span @click="() => slotProps.arr.push(3)">
{{ slotProps.arr }}
</span>
</template>
</Child>

完整例子

1
2
3
4
<!-- 子组件child中假设有数据ab = { a:1, b:2 } -->

<!-- 定义有具名插槽header,且绑定插槽props传递给父组件 -->
<slot name="header" :a="ab.a" :b="ab.b" :ab="ab">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- 父组件中调用子组件,可获取插槽作用域中绑定在插槽上的变量 -->
<Child>
<!-- 以前的命名插槽用slot,现在改用template -->
<template v-slot:header>
...
</template>

<!--
简写:
<template #header></template>

可获取子组件通过 插槽props 传给父组件的变量:
<template #header="slotProps">
<!-- 可使用子组件中变量 -->
{{ slotProps.a }}
{{ slotProps.b }}
</template>

<!-- 可解构 -->
<template #header="{ a, b }">
{{ a }}
{{ b }}
</template>
-->
</Child>

区分 匿名插槽 和 插槽prop

  • 匿名插槽使用方法:
    • 也可使用默认插槽的缩写语法,直接写在调用子组件内**<child>插槽内容</child>**
      1
      2
      3
      <child>
      <template v-slot:default>插槽内容</template>
      </child>
  • 插槽prop使用方法(例子参考}:
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 假设child内有数据ab={a:1,b:2}且<slot v-bind="a"> -->
    <child>
    <template v-slot:default="slotProps">插槽内容+slotProps.ab.a</template>
    <!-- 或者解构插槽prop -->
    <template v-slot:default="{ab}">插槽内容+ab.a</template>
    <!-- 可使用默认插槽的缩写语法 -->
    <template v-slot="{ab}">插槽内容+ab.a</template>
    </child>
  • 当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这时我们就可以把 v-slot 直接用在子组件上,注意!这种情况下 默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确
    • 例子里在Child标签上获取了一个slotProps,结果其中一个具名template又获取一次otherSlotProps,就乱了:
      1
      2
      3
      4
      5
      6
      7
      8
      <!-- 无效,会导致警告 -->
      <Child v-slot="slotProps">
      <!-- 正确写法:<template v-slot:default="slotProps"> -->
      {{ slotProps.childData.Name }}
      <template v-slot:other="otherSlotProps">
      slotProps is NOT available here
      </template>
      </Child >
  • 所以,只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <Child >
    <template v-slot:default="slotProps">
    {{ slotProps.childData.Name }}
    </template>

    <template v-slot:other="otherSlotProps">
    ...
    </template>
    </Child>

slot匿名/具名插槽

  • 自 2.6.0 起,slot被废弃
  • slot:组件的一块HTML模板
  • 子组件:规定显示的位置 (<slot></slot>)
    1
    2
    3
    4
    5
    6
    7
    <!-- 假设以下是子组件navigation-link的内容,slot就是插槽位置 -->
    <a
    v-bind:href="url"
    class="nav-link"
    >
    <slot></slot>
    </a>
  • 父组件:规定显示的HTML模板(内容)、显不显示 (Your Profile就是显示内容)
    1
    2
    3
    4
    <!-- 假设navigation-link是子组件名,Your Profile就是子组件中插槽位置显示的内容 -->
    <navigation-link url="/profile">
    Your Profile
    </navigation-link>
  • 上面的例子:当组件渲染的时候,**<slot></slot> 将会被替换为Your Profile**。

slot的name属性

  • 多个slot时,可以通过name来加以区分
  • 匿名插槽:一个不带 name 的 <slot> 会带有**隐含的名字default**。
  • 使用方法:在父组件中使用子组件中加入template组件里面用**v-slot指令定义name**,对应子组件中<slot>的name属性
    • 任何没有被包裹在具名插槽中(即带有 v-slot<template> 中)的内容都会被视为**默认插槽(default,即未命名的插槽)**的内容。

例子

  • 假设子组件为<base-layout>:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <div class="container">
    <header>
    <!-- 我们希望把页头放这里 -->
    <slot name="header"></slot>
    </header>
    <main>
    <!-- 我们希望把主要内容放这里,这是默认插槽,相当于name="default" -->
    <slot></slot>
    </main>
    <footer>
    <!-- 我们希望把页脚放这里 -->
    <slot name="footer"></slot>
    </footer>
    </div>
  • 父组件中,使用子组件时:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <base-layout>
    <!-- 以前的slot="header"在2.6.0以后被废除了 -->
    <template v-slot:header>
    <h1>Here might be a page title</h1>
    </template>

    <!-- 以下内容出现在 main 里,这是默认插槽,相当于name="default" -->
    <!-- <template v-slot:default>可有可无 -->
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>

    <template v-slot:footer>
    <p>Here's some contact info</p>
    </template>
    </base-layout>
  • 结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <div class="container">
    <header>
    <h1>Here might be a page title</h1>
    </header>
    <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
    </main>
    <footer>
    <p>Here's some contact info</p>
    </footer>
    </div>

v-slot可不在template上的唯一情况

  • 注意: v-slot 只能添加在 <template> (只有一种例外情况),这一点和已经废弃的 slot 不同。
    • 例外情况:只存在默认插槽(不存在其他具名插槽)时,子组件的标签才可以被当作插槽的模板来使用。才可以把 v-slot 直接用在子组件的标签上
      • 注意默认插槽的缩写语法 不能 和具名插槽混用,因为它会导致作用域不明确
1
2
3
4
5
6
<!-- 假设child是子组件 -->
<child v-slot:default="slotProps">
<!-- 也可以省略default,写成child v-slot="slotProps" -->
{{ slotProps.user.firstName }}
</child>
<!-- 注意:slotProps是child组件中所有 插槽prop 的对象的集合对象,插槽props指子组件<slot>插槽中绑定的属性 -->

插槽内容作用域

  • 插槽具体显示的内容是在父组件中规定的,所以插槽内容的作用域和父组件作用域一致
  • 例子(navigation-link是子组件):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- 模拟父组件调用子组件navigation-link的场景 -->
    <navigation-link url="/profile">
    Clicking here will send you to: {{ url }}
    <!--
    这里的 `url` 会是 undefined,因为其 (指该插槽的) 内容是
    _传递给_ <navigation-link> 的而不是
    在 <navigation-link> 组件*内部*定义的。
    -->
    </navigation-link>
  • 官方规则:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
    • 所以处于父级模板里的 子组件插槽内容 是在父级作用域中编译的
  • 但通过 插槽prop ,父组件可以在调用子组件中的数据

slot-scope作用域插槽(带数据的具名插槽)

  • 自 2.6.0 起有所更新,slot和slot-scope已废弃统一使用v-slot代替!
  • 具体例子参考
  • 与slot异同
    • 与slot相同,插槽位置 在子组件中通过<slot>规定
    • 与slot不同:
      • 区别于 具名插槽的v-slot:header作用域插槽使用 slot属性slot="header"
        • slot插槽内容 是在使用子组件时的带了v-slot的template组件里面写;slot-scope是在带了slot属性(默认default可省略)的template/非template里面写插槽内容。
      • 区别于 具名插槽的v-slot:default="slotProps"作用域插槽的 slot-scope属性slot-scope="childData" 可直接获取 插槽propchildData
      • 具名插槽只有在只存在默认插槽(不存在其他具名插槽)时,才可以把 v-slot 直接用在 非template组件 的标签上,不过作用域插槽的slot可以出现在 非template组件 上作为属性
  • 子组件内的作用域插槽上使用slot-scope 属性,可以接收子组件内插槽绑定的传递给父组件的数据(即插槽prop):
    1
    2
    3
    4
    5
    6
    <child>
    <template slot="default" slot-scope="slotProps">
    <!-- 插槽内容,slot="default"可省略 -->
    {{ slotProps.msg }}
    </template>
    </child>
  • slot-scope属性也可以**直接用于非 <template> 元素 (包括组件上)**:
    1
    2
    3
    4
    5
    <child>
    <span slot-scope="slotProps">
    {{ slotProps.msg }}
    </span>
    </child>

例子

  • 子组件中:
    1
    2
    3
    4
    5
    6
    7
    <div class="child">
    <div>
    <!-- 插槽命名为default,绑定数据msg给父组件的插槽prop -->
    <slot name="default" :msg="msg"> </slot>
    <p>这里是child 组件</p>
    </div>
    </div>
  • 父组件中:
    1
    2
    3
    4
    5
    6
    7
    <child >
    <!-- 区别于 具名插槽的v-slot:default,作用域插槽使用 slot属性 -->
    <!-- 区别于 具名插槽的v-slot:default="childData",作用域插槽的 slot-scope属性 可直接获取 插槽prop -->
    <div slot="default" slot-scope="childData">//作用域插槽的用法(slot-scope)
    {{ childData.msg }}
    </div>
    </child >

插槽内容作用域

插槽 prop(常命名为slotProps)子组件传值给父组件使用插槽位置

  • 插槽 prop
  • 插槽内容和父组件享有同样的作用域,但无法获取子组件内部的数据,但父组件中的插槽内容可通过插槽 prop获取子组件内的数据
  • 注意:slotProps可自定义命名,是子组件中所有 插槽prop 的对象的集合对象
    • 插槽prop:子组件插槽中绑定的属性
  • 例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- 子组件中,给插槽绑定user属性 -->
    <slot v-bind:user="user" v-bind:password="password">
    {{ user.lastName }}
    </slot>

    <!-- 父组件中,可获取所有插槽绑定属性(即子组件内数据)的集合对象 -->
    <child v-slot:default="slotProps">
    <!-- 也可以省略default,写成v-slot="slotProps"。 -->
    <!-- 也可以解构,写成v-slot="{ user, password }" -->
    {{ slotProps.user.firstName }}
    </child>
    <!-- 注意:slotProps是child组件中所有 插槽prop 的对象的集合对象,插槽props指子组件<slot>插槽中绑定的属性 -->
  • 注意: 如果使用v-bind="{a: 1, b: 2}"直接绑定插槽属性,则插槽调用是获取到的 插槽prop 直接就是{a: 1, b: 2},而不是{{a: 1, b: 2}}
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 子组件中,给插槽绑定user属性 -->
    <slot v-bind="{a: 1, b: 2}">
    </slot>

    <!-- 父组件中,可获取所有插槽绑定属性(即子组件内数据) -->
    <child v-slot="{a, b}">
    {{ a }} // 1
    </child>

父组件插槽中修改slotProps是否影响子组件中状态

  • 当子组件中给插槽绑定对象/数组,父组件中修改对象/数组(非整个替换)时,会影响子组件中状态
  • 下面例子中,改变a/b或者整个替换掉ab、arr@click="() => slotProps.arr = [3]"都不会影响子组件中变量
1
2
3
<!-- 子组件,给插槽绑定ab对象和arr数组 -->
<!-- arr: [0, 1], ab: { a:1, b:2 }, -->
<slot name="header" :a="ab.a" :b="ab.b" :ab="ab" :arr="arr"></slot>
1
2
3
4
5
6
7
8
9
<!-- 父组件 -->
<Child>
<template #header="slotProps">
<!-- 点击将改变子组件中arr -->
<span @click="() => slotProps.arr.push(3)">
{{ slotProps.arr }}
</span>
</template>
</Child>

独占默认插槽的缩写语法不能和具名插槽混用

  • 参考文档
  • 当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这时我们就可以把 v-slot 直接用在子组件上,但此时 默认插槽的缩写语法 不能和 具名插槽 混用,因为它会导致作用域不明确
    • 例子里在Child标签上获取了一个slotProps,结果其中一个具名template又获取一次otherSlotProps,就乱了
      1
      2
      3
      4
      5
      6
      7
      <!-- 无效,会导致警告 -->
      <Child v-slot="slotProps"><!-- 插槽prop使用了默认插槽的缩写语法 -->
      {{ slotProps.childData.Name }}
      <template v-slot:other="otherSlotProps"><!-- 具名插槽也想获取一个slotProps -->
      slotProps is NOT available here
      </template>
      </Child >
  • 只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <Child>
    <template v-slot:default="slotProps">
    {{ slotProps.childData.Name }}
    </template>

    <template v-slot:other="otherSlotProps">
    ...
    </template>
    </Child>

参考