Home > Archives > slot思想、用法及原理详解

slot思想、用法及原理详解

Publish:

slot的意思是插槽,它主要是用来做内容分发的工具,那么什么是内容分发呢?slot又有几种用法?slot的每种用法主要是用于解决什么问题?

可能在学习Vue的文档时,不少人都会有这样的疑问。

今天我就来总结一下slot思想、用法及原理。

slot

首先我们需要明确的是Vue的组件化思想。

组件的意义在于复用,所以我们在写出一个组件后总是希望父组件用某种方式来调用它,for instance:

<child-component :para="xxx" @evt="yyy"></child-component>

这样的做法是Vue推荐的,我们可以在使用时像子组件prop参数,也可以监听子组件emit的事件。

但是存在一个问题,子组件的内部对父组件是不透明的。

这就意味着父组件没有办法自由控制子组件的部分行为,无法将它的内容直接分发到子组件,无法在调用组件时实现定制化。

于是slot就可以发挥作用了。

举一个例子,假设我们有一个btn组件,它的意义在于被各个组件复用,不同的父组件button可能有不同的文字,我们可能会想到这样写:

<template id="btn-template">
    <button>{{ text }}</button>
</template>
Vue.component('btn', {
    template: '#btn-template',
    props: ['text']
})

然后父组件在调用时:

<btn text="删除"></btn>

<btn text="添加"></btn>

这样做不是不可以,但存在问题,一是如何处理prop数据是由子组件控制的,父组件无法操控,不直观也不透明;二是父组件需要分发较为复杂的模板内容时,子组件会存在许多复杂的DOM操作逻辑。

我们可以用slot来解决这个问题。

<template id="btn-template">
    <button>
        <slot>
            默认的内容
        </slot>
    </button>
</template>
Vue.component('btn', {
    template: '#btn-template'
})

在调用时:

<btn>删除</btn>

<btn>添加</btn>

子组件中默认的内容只有在父组件没有分发内容时才会渲染,如果父组件有分发的内容,则会被覆盖。

上述的调用会渲染成:

<button>删除</button>

<button>添加</button>

命名slot

可能讲的有点啰嗦了,不过基本的思想阐述清楚了。

下面尽量简洁一点。

命名slot,顾名思义,这个插槽是有名字的。

之所以要有名字,是为了方便父组件分发多个内容,并在子组件中正确渲染。

这个例子官方文档举的很好,假设子组件的模板是这样:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

看到这个模板应该已经清楚了。

我们需要的就是父组件在调用时指明要分发的内容给分别名为header和footer的插槽,指明的过程也很简单,给分发内容加上一个相应值的slot特性即可。

父组件中没有具名的分发内容,会被渲染到不具名的slot中。

比如我们这样调用子组件:

<app-layout>
  <h1 slot="header">这里可能是一个页面标题</h1>
  <p>主要内容的一个段落。</p>
  <p>另一个主要段落。</p>
  <p slot="footer">这里有一些联系信息</p>
</app-layout>

最终渲染的结果是这样的:

<div class="container">
  <header>
    <h1>这里可能是一个页面标题</h1>
  </header>
  <main>
    <p>主要内容的一个段落。</p>
    <p>另一个主要段落。</p>
  </main>
  <footer>
    <p>这里有一些联系信息</p>
  </footer>
</div>

作用域slot

作用域slot可能就没有那么见名知义了。

我的想法是,作用域slot的意思就是,将分发的内容限制在插入插槽的作用域内,这个作用域中的数据既可以是自己定义的变量,也可以是prop得到数据。

这在分发内容需要依靠复杂数据时特别有用(如渲染表格、列表等),举一个例子,假设我们子组件中有一个插槽列表:

<ul>
  <slot 
    name="item"
    v-for="item in items"
    :text="item.text"></slot>
</ul>

从这一段模板我们可以看出,子组件中有多个同名的slot。

那么如何把父组件分发的内容限制在其对应插槽的作用域内呢?

也很简单,在父组件的分发内容中加入一个slot-scope特性就可以了。

这个slot-scope特性的值指向这个插槽的作用域对象(也可以使用解构~)

比如在调用这个子组件时,分发内容可以这样写:

<my-awesome-list :items="items">
  <li
    slot="item"
    slot-scope="props"
    class="my-fancy-item">
    {{ props.text }}
  </li>
</my-awesome-list>

假设items是一个数组:['hello', 'world']

渲染结果会是这样的:

<ul>
  <li>hello</li>
  <li>world</li>
</ul>

需要注意的一点是,在Vue 2.5以前,slot-scope特性必须在template标签上使用,Vue 2.5以后,slot已经可以用于任意的元素和组件中了。

在render函数中渲染slot

讲完了在template中的slot用法,我们来讲一讲在render函数中如何使用slot接收分发的内容。

事实上template是被编译成render函数以生成VNode的,所以本文接下来也可以看做是对slot工作原理的阐述。

在render函数中使用slot接收分发的内容,我们需要首先熟悉一些实例属性API。

我们要知道的是,访问分发给插槽的内容,需要调用vm.$slots这个API。

比如,slot="foo"中的内容可以在vm.$slots.foo中找到,default属性包括了所有没有被包含在具名插槽中的分发内容。

用官方文档举一个例子:

Vue.component('blog-post', {
    render(createElement) {
        const header = this.$slots.header
        const body = this.$slots.default
        const footer = this.$slots.footer
        return createElement('div', [
            createElement('header', header),
            createElement('main', body),
            createElement('footer', footer)
        ])
    }
})

在调用子组件时我们就可以正常的分发内容了:

<blog-post>
  <h1 slot="header">
    About Me
  </h1>
  <p>Here's some page content, which will be included in vm.$slots.default, because it's not inside a named slot.
  </p>
  <p slot="footer">
    Copyright 2016 Evan You
  </p>
  <p>If I have some content down here, it will also be included in vm.$slots.default.</p>.
</blog-post>

分发内容会被渲染到它应在的位置。

在render函数中分发内容

render函数可以用来渲染子组件,当然也可以在父组件中调用子组件,而在调用子组件时,自然可以向其分发内容,举个例子:

render(createElement) {
    return createElement('div', [
        createElement('child', [
            createElement('p', '这是一个不具名slot'),
            createElement('p', {
                slot: 'main'
            },
            '这是一个名为main的slot')
        ])
    ])
}

如此分发内容给插槽,可以使内容分发有别于prop,更加清晰(I guess…)

这个render函数返回的VNodes如下:

<div>
  <child>
    <p>这是一个不具名slot</p>
    <p slot="main">这是一个名为main的slot</p>
  </child>
</div>

在render函数中渲染作用域插槽

在render函数中使用作用域插槽同样需要我们对实例属性vm.$scopedSlots有一定的理解。

vm.$scopedSlots用来访问作用域插槽接收到的分发内容,包括默认插槽在内的每一个插槽接收到的分发内容,都可用该对象访问,该对象会包含一个返回相应VNode的函数(类似于一个createElement函数)。

比如我们这样写一个render函数:

return createElement('div', [
    this.$scopedSlots.default({
        text: this.defaultMsg
    }),
    this.$scopedSlots.foo({
        property: this.fooMsg
    })
])

上面这一段render函数相当于这样一个模板:

// child
<div>
  <slot :text="defaultMsg"></slot>
  <slot :property="fooMsg" name="foo"></slot>
</div>

于是我们可以这样给它分发内容:

<child>
  <p slot-scope="props">{{ props.text }}</p>
  <!-- 下面使用了解构语法 -->
  <p slot-scope="{ property }" slot="foo">{{ property }}</p>
</child>

在render函数中分发内容给作用域插槽

在render函数中分发内容给作用域插槽,可以使用createElement函数的data参数对象的scopedSlots属性。

还是以一个例子来说明:

render(createElement) {
    return createElement('div', [
        createElment('child', {
            scopedSlots: {
                default: props => createElement('span', props.text)
            }
        })
    ])
}

如此分发内容给作用域插槽,可以使内容分发有别于props,更加清晰,同时也是由于Vue 2.5之前slot-scope必须写在template标签上(I guess…)

事实上,上面这一段渲染函数渲染出的VNodes如下:

<div>
  <child>
    <span slot-scope="props">{{ props.text }}</span>
  </child>
</div>

结语

这篇文章从内容分发/slot的思想讲到用法,最后讲到在render函数中的实现原理,我感觉写的还是比较清晰的。

也希望读者可以有一定收获。

声明: 本文采用 BY-NC-SA 授权。转载请注明转自: slot思想、用法及原理详解 - 无火的余灰