你知道Vue 进阶中的这个知识点吗?从 slot 到无渲染组件

什么是插槽

插槽(slot)通俗的理解就是“占坑”,在组件模板中占有位置,当使用该组件的时候,可以指定各个坑的内容。也就是我们常说的内容分发

在 Vue 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slotslot-scope,这两个目前已被废弃但未被移除且仍在文档中的 attribute

本文的例子基于 Vue 2.6.X,所以用的都是 v-slot 的语法。

默认插槽

我们新建父组件 Parent 和子组件 Child,结构如下:

父组件:

<!-- 默认插槽 -->
<h3>默认插槽</h3>
<Child>
    <div class="parent-text">Hi, I am from parent.</div>
</Child>

子组件:

<div class="child">
    <slot></slot>
    <div>Hello, I am from Child.</div>
</div>

父组件调用 Child 组件的时候,会在 Child 标签中将内容传入到子组件中的 <slot> 标签中,如下所示

从 slot 到无渲染组件

也就是最后的渲染结果如下:

<div class="child">
    <div class="parent-text">Hi, I am from parent.</div>
    <div>Hello, I am from Child.</div>
</div>
从 slot 到无渲染组件

后备内容

我们可以在子组件中的 <slot> 中加入一些内容,像下面一样

<div class="child">
  <slot>当父组件不传值的时候,我就展示,我只是一个后备军</slot>
  <div>Hello, I am from Child.</div>
</div>

当父组件调用的时候, 子组件标签内没有相关的内容时候,<slot> 标签内的内容就会生效,否则就不会渲染,可以理解就是个“备胎”

如父组件调用上面子组件:

    <!-- 后备内容 -->
    <h3>后备内容</h3>
    <Child1></Child1>

结果如下:

具名插槽

当然,插槽可以不止一个,这个主要是为了能够灵活的控制插槽的位置以及组件的抽象。我们可以通过在子组件的 slot 标签中设置 name 属性,然后在父组件中通过 v-slot:(或者使用简写 #) + 子组件 name 属性值的方式指定要插入的位置。如果是默认插槽的话,v-slot:default 即可

如下父组件:

    <!-- 具名插槽 -->
    <h3>具名插槽</h3>
    <Child2>
      <template v-slot:footer><div>我是底部</div></template>
      <template #header><div>我是头部</div></template>
      <template v-slot:default>
        <div>我是内容</div>
      </template>
    </Child2>

子组件

  <div class="child">
    <slot name="header"></slot>
    <slot></slot>
    <div>Hello, I am from Child.</div>
    <slot name="footer"></slot>
  </div>

需要留意的是,最后渲染的顺序是以子组件的顺序为主,也就是上面的例子,渲染出来如下:

作用域插槽

有时候,我们想在一个插槽中使用子组件的数据和事件,类似如下(注意: user 是定义在 Child3 组件中的数据):

    <Child3>
      <template>
        <div>我的名字:{{user.name}}</div>
        <div>我的年龄:{{user.age}}</div>
        <button @click="callMe">Clicl Me</button>
      </template>
    </Child3>

会直接报错:

原因在于父组件取不到子组件的数据,这里记住一个原则:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

那我们怎样才能获取到子组件的数据或者事件呢?我们可以直接在子组件中通过 v-bind 的方式将数据或者事件传递给父组件中,如下所示

  <div class="child">
    <div>Hello, I am from Child.</div>
    <!-- 将user和callMe通过 v-bind 的方式传递 -->
    <slot :user="user" :callMe="callMe"></slot>
  </div>

然后在父组件中的插槽内,通过类似 v-slot:default="slotProps" 接受子组件传递过来的数据

    <Child3>
      <!-- slotProps 可以自定义-->
      <template v-slot:default="slotProps">
        <div>我的名字:{{slotProps.user.name}}</div>
        <div>我的年龄:{{slotProps.user.age}}</div>
        <button @click="slotProps.callMe">Clicl Me</button>
      </template>
    </Child3>

以上 slotProps 可以自定义,而且可以使用解构赋值的语法

<!-- 解构赋值 -->
<template v-slot:other="{ user, callMe}">
  <div>我的名字:{{user.name}}</div>
  <div>我的年龄:{{user.age}}</div>
  <button @click="callMe">Clicl Me</button>
</template>

实例:解耦业务逻辑和视图

我们经常会遇到一个场景,就是两个组件的业务逻辑是可以复用的,但是视图却不一样,比如我们经常会有类似切换开关的需求,功能包括:

  • 关闭开关
  • 打开开关
  • 切换开关
  • 开关关闭或者打开的时候不一样的内容

我们可以很快的写出它的一个 JS 业务逻辑代码:

export default {
  data() {
    return {
      currentState: this.state
    }
  },
  props: {
    state: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    openState() {
      this.currentState = true;
    },
    closeState() {
      this.currentState = false;
    },
    toggle() {
      this.currentState = !this.currentState;
    }
  }
}

但是可能现在我的样式一是这样的

然而另外一个地方的样式是这样的(只是举个例子,现实可能更加的复杂,甚至有可能一些按钮直接就隐藏掉了)

这个时候,插槽就派上了用场。上面提到作用域插槽可以将数据和事件从子组件传递给父组件,这就相当于对外暴露了接口。而且可以将 HTML 中的 DOM 以及 CSS 交给父组件(调用方)去维护,子组件通过 <slot> 标签插入的位置即可,主要逻辑如下:

子组件:

<template>
  <div class="toggle-container">
    <slot :currentState="currentState" :setOn="openState" :setOff="closeState" :toggle="toggle"></slot>
  </div>
</template>

父组件:

<Toggle1 :state="state" class="toggle-container-two">
  <template v-slot:default="{currentState, setOn, setOff, toggle }">
    <button @click="toggle">切换</button>
    <button @click="setOff">关闭</button>
    <button @click="setOn">打开</button>
    <div v-if="currentState">我是打开的内容</div>
    <div v-else>我是关闭的内容</div>
  </template>
</Toggle1>

我们现在采用的是单文件的方式书写的,实际上子组件还是会有相关的 HTML 结构,如何做到子组件完全不需要渲染自己的 HTML 呢?那得了解下无渲染组件的实现

进阶:无渲染组件的实现

无渲染组件(renderless components)是指一个不需要渲染任何自己的 HTML 的组件。相反,它只管理状态和行为。它会暴露一个单独的作用域,让父组件或消费者完全控制应该渲染的内容。Vue 中,提供了单文件组件的写法。像上面的示例一样,我们始终还是在子组件中进行了一些渲染的操作,那如何做到真正的不渲染组件呢?

比如上面的 toggle 例子,我们已经做到了子组件暴露一个单独的作用域,让父组件或消费者完全控制应该渲染的内容。现在我们需要将单文件中的 template 结构(slot 标签外层的 div)完全交给父组件,但单文件组件中 slot 标签是不能作为 template 的根元素的

这个时候,我们需要了解一下 Vue 渲染函数(render function)

归根结底,Vue 及其所有的组件都只是 JavaScript。单文件组件最后会被构建工具,如 webpack,将 CSS 抽取形成一个文件,其他的内容会被转换成 JavaScript,类似如下:

export default {
  template: <div class="mood">...</div>,
  data: () => ({ todayIsSunny: true })
}

当然,这个不是它的最终形态,模板编译器会提取 template 属性内容并将其内容编译为 JavaScript,然后通过 render 函数添加到组件对象中。最终形态应该是如下:

render(h) {
  return h(
    'div',
    { class: 'mood' },
    this.todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me'
  )
}

具体的渲染函数可参见官网,虽然写 render 函数的成本会高一些,但是它的性能会比单文件组件好很多。

以上的例子,只有插槽的时候,我们只需要在 render 函数中,使用 this.$scopedSlots.default 代替掉 <slot> 标签即可

代码如下:

export const toggle = {
  data() {
    return {
      currentState: this.state
    }
  },
  render() {
    return this.$scopedSlots.default({
      currentState: this.currentState,
      setOn: this.openState,
      setOff: this.closeState,
      toggle: this.toggle,
    })
  },
  props: {
    state: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    openState() {
      this.currentState = true;
    },
    closeState() {
      this.currentState = false;
    },
    toggle() {
      this.currentState = !this.currentState;
    }
  }
}

以上就可以做到子组件完全不渲染自己的 HTML

总结

本文介绍了一些 Vue 插槽的基本知识,包括

  • 默认插槽
  • 后备内容
  • 具名插槽
  • 作用域插槽

然后介绍了一下,如何通过插槽实现业务逻辑和视图的解耦,再结合渲染函数实现真正的无渲染函数,好了,今天的分享就到这里,如果你是正在学习前端或准备学习前端,可以去我的前端学习交流裙(109029339)免费下载一些前端学习视频,而且不定时还有大咖直播分享,希望能帮助大家共同成长。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342