浅谈Vue作用域插槽

6,055 阅读6分钟

前言

在Vue的官方文档中对于插槽的描述比较凝练不容易理解,作者在刚开始接触插槽时也是一头雾水,在实践中好像也可以简单的使用,但是想把插槽用的得心应手就比较困难了。

最近因为工作需求需求手动封装一个类似ElementUI的级联选择器的组件,在开发过程中一直不太明白它的自定义节点功能:

<el-cascader :options="options">
  <template slot-scope="{ node, data }">
    <span>{{ data.label }}</span>
    <span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
  </template>
</el-cascader>

后来又去仔细研究了一下作用域插槽,最后终于成功的开发完成了,下面想和大家分享一下我通过开发这个组件对于插槽的一些新的理解,由于本人才疏学浅,所以有什么不妥的地方还希望各位大佬们指正。

下面我主要分为三部分讲解,默认插槽、具名插槽和作用域插槽,默认插槽和具名插槽比较容易理解所以下面不用解释太多,主要是分享一些对作用域插槽的理解。

默认插槽

默认插槽又被称为单个插槽、匿名插槽,默认插槽的使用是最方便的,只需要在子组件需要插入父组件传入内容的地方加上即可,下面通过一个简单的例子展示一下:

父组件:

<template>
    <div>
        <h1>这里是父组件</h1>
        <child>
            <h3>这里是传递的内容</h3>
        </child>
    </div>
</template>

子组件:

<template>
    <div>
        <h2>这里是子组件</h2>
        <slot></slot>
    </div>
</template>

渲染的dom:

<div>
    <h1>这里是父组件</h1>
    <div>
        <h2>这里是子组件</h2>
        <h3>这里是传递的内容</h3>
    </div>
</div>

页面显示: cdf.png 是不是很简单,但是默认插槽有个缺陷,你如果现在父组件中传递两部分内容并且分别渲染在不同的位置,这个默认插槽是做不到了,它会渲染所有传递的内容。像这样: 父组件:

    <div>
        <h1>这里是父组件</h1>
        <child>
            <h3>这里是头部插槽</h3>
            <h3>这里是底部插槽</h3>
        </child>
    </div>
</template>

子组件:

<template>
    <div>
        <h2>这里是子组件</h2>
        <p>这里是头部,下面是插槽</p>
        <slot></slot>
        <p>这里是底部,下面是插槽</p>
        <slot></slot>
    </div>
</template>

效果图:

abc.png 可以发现传递的内容在子组件中全部渲染了,而且两部分一样,这并不是我们想要的,为了解决这种问题,也为了使组件能够更灵活的复用,具名插槽的价值就体现出来了,下面来介绍一下具名插槽。

具名插槽

具名插槽就是有名字的插槽,就和我们的名字一样,用来区分插入的位置,这样就解决我们上面的问题。
父组件:

<template>
    <div>
        <h1>这里是父组件</h1>
        <child>
            <h3 slot="head">这里是头部插槽</h3>
            <h3 slot="footer">这里是底部插槽</h3>
        </child>
        <!-- 自Vue2.6起,语法变成了这样 -->
        <child>
            <template v-slot:head>
                <h3>这里是头部插槽</h3>
            </template>
            <template v-slot:footer>
                <h3>这里是底部插槽</h3>
            </template>
        </child>
    </div>
</template>

子组件:

<template>
    <div>
        <h2>这里是子组件</h2>
        <p>这里是头部,下面是插槽</p>
        <slot name="head"></slot>
        <p>这里是底部,下面是插槽</p>
        <slot name="footer"></slot>
    </div>
</template>

效果图:

789.png 接下来主角要登场了,来揭开作用域插槽的神器面纱。

作用域插槽

官网是这样描述的:有时让插槽内容能够访问子组件中才有的数据是很有用的。显然作用于插槽的作用就是使父组件能够引用子组件中的数据。
下面用一个简单地例子展示一下:
父组件:

<template>
    <div>
        <h1>这里是父组件</h1>
        <child>
            <template slot-scope="slotsProps">
                {{slotsProps.info}}
            </template>
        </child>
        <!-- 自Vue2.6起,语法变成了这样 -->
        <child>
            <template v-slot:default="slotProps">
                {{slotProps.info}}
            </template>
        </child>
    </div>
</template>

子组件:

<template>
    <div>
        <h2>这里是子组件</h2>
        <slot :info="info"></slot>
    </div>
</template>

效果图:

cdf.png 可以看到成功渲染了子组件中的数据,可能有人会问既然要渲染子组件中的数据那直接写在子组件中就好了,何必通过作用域插槽传递数据给父组件然后再进行渲染了。

有这样一个场景,一个级联选择器,根据不同的应用场景渲染不同的变量,而且在不同的场景下可以进行不同而操作,比如编辑、删除等,那么想用这个组件是不是要对传入的数据的格式做严格的规定,这样组件的复用性是不是就变得很差了呢?但是通过作用域插槽就可以实现了呢。下面用一个简单地例子解释:

父组件:

<template>
  <div>
    <div class="container">
      <child :options="options" class="child">
        <template slot-scope="slotsProps">
          <span>{{ slotsProps.data.label }}</span>
          <button @click="getInfo(slotsProps.data)">编辑</button>
        </template>
      </child>
      <child :options="options" class="child">
        <template slot-scope="slotsProps">
          <span>{{ slotsProps.data.value }}</span>
          <button @click="getInfo(slotsProps.data)">删除</button>
        </template>
      </child>
    </div>
  </div>
</template>

子组件:

<template>
  <div>
    <div v-for="item in options" :key="item.value">
      <slot :data="item"></slot>
    </div>
  </div>
</template>

效果图:

456.png 惊喜的发现同样一个组件完全可以在不同的场景下复用,下面我们来看一下作用域插槽的数据传递。

作用于插槽的数据传递

在学习插槽时一直不太明白作用域插槽到底是什么,下面我总结了几点来理解作用域插槽。

插槽的slot写在哪里?作用域插槽和其他插槽一样,slot都是写在父组件中,slot是子组件暴露给父组件的接口,需要父组件通过slot传值给子组件。 作用域插槽,字面理解插槽的作用域为这个插槽,只对这个插槽生效。 父组件中引用数据时的属性名和子组件中赋值的属性名有什么关系?slot-scope="slotsProps这里定义了插槽的内容使用slotsProps来承接;{{ slotsProps.data.label }}这里是引用传给插槽的数据data对象的label属性;<slot :data="item"></slot>这里把item对象赋值给data。 注意:子组件中的data要和父组件中的data保持名称一致

那么上面这个例子的数据传递的步骤是

父组件将options传递给子组件 子组件把item传递给插槽,并暴露给父组件 父组件调用子组件插槽传递过来的数据 父组件调用子孙组件中的数据 有时我们发现在子组件中并拿不到要传给插槽的数据,可能要在子孙组件中的才可能拿到,这时我们应该怎么办呢? dome.vue

<template>
  <div>
    <span>{{value}}</span>
    <div class="container">
      <parent :options="options" class="child">
        <template slot-scope="slotsProps">
          <span>{{ slotsProps.data.label }}</span>
          <button @click="getInfo(slotsProps.data)">获取Value</button>
        </template>
      </parent>
    </div>
  </div>
</template>

父组件:

<template>
  <div>
    <div v-for="item in options" :key="item.value">
      <child :info="item">
        <slot></slot>
      </child>
    </div>
  </div>
</template>

子组件:

<template>
  <div>
    <slot :data="info"></slot>
  </div>
</template>

我起初是这样写,结果报错了

0109.png 意思就是在渲染slot时并没有获取到data对象,突然明白过来,在渲染到父组件时并没有给data赋值,所以demo.vue就开始报错了,那怎么样让父组件不渲染slot,在渲染子组件时才渲染呢,这里我是使用:render-label="scopedSlots.default"向子组件传值的,而scopedSlots.default"向子组件传值的,而scopedSlots.default就是作用域插槽了,那么这样在子组件中怎么调用这个方法,这里我是使用render函数实现的。才疏学浅没有想到其他的解决方法,希望各位大佬指教。

下面看一下我改正后的代码: 父组件:

<template>
  <div>
    <div v-for="item in options" :key="item.value">
      <child :render-label="$scopedSlots.default" :info="item">
      </child>
    </div>
  </div>
</template>

子组件

render(h) {
    return(
      <span>
        {this.renderLabel({data: this.info})}
      </span>
    )
}

效果图:

aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS85LzMwLzE2ZDgxNzBiN2YxYTZjMTY.png

后备内容

最后来补充一下后备内容,后备内容是指在父组件没有给slot传值时被渲染的内容,具体实现如下:

<!-- 默认插槽 -->
<slot>
    <span>后备内容</span>
</slot>
<!-- 具名插槽 -->
<slot name="head">
    <span>后备内容</span>
</slot>
<!-- 作用域插槽 -->
<slot :data="item">
    <span>后备内容</span>
</slot>

最后

原文链接:blog.csdn.net/weixin_4092…