Skip to content
On this page

组件进阶和插槽

学习目标

  • 【掌握】Vue中动态组件
  • 【掌握】keep-alive的使用
  • 【掌握】Vue中的组件通信
  • 【了解】Vue中的ref和$nextTick
  • 【掌握】Vue中插槽的基本使用
  • 【掌握】Vue中插槽的深入理解

前面已经强调组件系统是Vue的核心,Vue中除开可以通过单文件组件来自定义组件之外,还内置了一批全局组件可以直接使用,接下来我们来学习这些内置组件的使用。

1. 动态组件

有的时候,在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面里:

p9EJYee.png

利用vue中的内置组件<component>可以实现这种动态切换:

vue
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component :is="currentTabComponent"></component>

在上述示例中,currentTabComponent 可以包括

  • 已注册组件的名字 (绝大多数都采用这种方式)
  • 一个组件的选项对象

整体代码如下:

vue
//App.vue
<template>
  <div id="app">
    <div class="title">
      <span @click="fn1">Home</span>
      <span @click="fn2">Post</span>
      <span @click="fn3">Archive</span>
    </div>
    <component :is="currentTabComponent" />
  </div>
</template>

<script>
  import Home from '@/components/Home.vue'
  import Post from '@/components/Post.vue'
  import Archive from '@/components/Archive.vue'
  export default {
    name: 'App',
    components: {},
    data() {
      return {
        currentTabComponent: Home,
      }
    },
    methods: {
      fn1(){
        this.currentTabComponent=Home
      },
      fn2(){
        this.currentTabComponent=Post
      },
      fn3(){
        this.currentTabComponent=Archive
      }
    },
  }
</script>

<style scoped>
  .title span {
    padding: 8px;
    background-color: #ccc;
    border: 1px solid #ccc;
  }
</style>

2.keep-alive

我们刚才使用了<component>组件实现了组件之间的动态切换,每次切换都会重新执行组件的创建销毁过程,这样既浪费性能又无法缓存住组件的状态,通过<keep-alive>这个内置组件我们可以解决这个问题,主要使用<keep-alive>的时候必须保证每个组件都有自己的name配置项。

vue
<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>

为了验证效果成功了,我们可以这样做:

  • 将3个组件设置为3个input框,切换组件时input框里的内容被正确保留了。

    p9Ea8VU.png

  • 在每个组件中调用生命周期函数,会发现创建销毁的钩子没有调用,此时会调用2个新的生命周期函数activated和deactivated

3.组件通信

前面我们已经学习了利用props和自定义事件实现父子组件之间的数据通信,现在来对这个话题做进一步探讨:

3.1 父组件—>子组件 props

之前有详细讨论,此处不再赘述。

3.2 子组件—>父组件 $emit()

之前有详细讨论,此处不再赘述。

3.2 父级组件—>后代组件 provide/inject

如果组件的层级变得更加复杂,父组件中包裹子组件,子组件中继续包裹后代组件,这样的通信应该如何完成呢?当然可以利用props逐层传递,但这样又显得太过繁琐,Vue还提供了一组配置选项provide/inject来实现父级组件向后代组件传递数据。

在父组件中通过一个新的配置选项provide定义了一个数据msg:

vue
provide: {
      msg: '父组件中的信息',
}

在所有的后代组件(包括子组件中),可以通过配置项inject来接受这个msg,然后可以直接在模板中使用msg:

vue
inject: ['msg'],
vue
<div class="subchild">
    这是后代组件
    {{ msg }}
</div>

3.3 任意组件间的通信—>VueX

更多业务场景下,我们希望组件之间没有父子关系的约束可以随意通信,在后面我们会单独来说明如何利用公共状态管理库VueX/Pinia来实现。

除此之外,还有一些通信方式例如 全局事件总线 发布订阅等等,后续如有使用到,我们再专门说明,我们必须熟练掌握props和自定义事件,这是最常用的方式,必须明确一个思想:数据在哪 修改数据的方法就在哪

4.ref

在使用Vue的过程中,我们的思想是操作数据而非DOM结构,但是Vue仍然提供了一个ref属性来引用DOM结构或者组件。

vue
<!-- 通过this.$refs.p可以访问到这个p标签 -->
<p ref="p">hello</p>

<!-- 通过this.$refs.child可以访问到这个组件实例 -->
<child-component ref="child"></child-component>

5.$nextTick

Vue会在数据更新后进行响应式地视图更新,但是这个更新并非及时的,如果希望将逻辑放到下次视图更新后执行,可以调用$nextTick这个方法来实现,下面用一个案例来说明这一点:

现在希望点击按钮能够切换文本的编辑状态,并且进入到编辑状态时应该能自动获取焦点:

我们可以通过ref获得input输入框,通过focus方法来获得焦点,代码应该如下:

vue
<template>
  <div class="app">
    <div class="left">
      <div
        v-if="!isEdit"
        class="t1"
      >
        {{ msg }}
      </div>
      <div
        v-else
        class="t2"
      >
        <input
          ref="ref1"
          v-model="msg"
          type="text"
        >
      </div>
    </div>
    <div class="right">
      <button @click="edit">
        {{ isEdit ? "确定" : "编辑" }}
      </button>
    </div>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      msg: 'hello',
      isEdit: false,
    }
  },
  methods: {
    edit() {
      this.isEdit = !this.isEdit
      if(this.isEdit){
        this.$refs.ref1.focus()
      }
    },
  },
};
</script>

<style scoped>
.app {
  background-color: #ddd;
  padding: 8px;
  display: flex;
}
.left {
  width: 30%;
  margin-right: 20px;
}
.t1 {
  border: 1px solid #000;
}
.t2 input {
  width: 100%;
}
</style>

此时点击切换会发现聚焦行为并没有发生,控制台也报出了没有获得到DOM元素的错误。

原因就在于在edit方法中执行this.isEdit=!this.isEdit的时候视图并没有马上更新,input并没有被马上加入到DOM结构了,所以我们应该希望这个聚焦行为发生在DOM结构更新之后,可以用刚才的实例方法$nextTick来实现,对应的代码改写一下:

js
edit() {
  this.isEdit = !this.isEdit
  this.$nextTick(function(){
    if(this.isEdit){
    this.$refs.ref1.focus()
  }
  })
},

聚焦效果正确实现了!

6.插槽基础

通过组件学习,我们已经掌握了如何利用prop向子组件传递数据,现在如果希望传递的是HTML结构呢?Vue给我们提供了插槽这个解决方案,一起来学习下如何使用。

vue
//父组件中的结构
<template>
  <div class="app">
    <Child>
      <h1>这是传递给子组件的标题</h1>
    </Child>
  </div>
</template>

传递给子组件的HTML结构写在了组件标签之间,之前并没有这样写过,需要留意。

vue
//子组件中的结构
<template>
    <div>
        这是子组件
        <slot></slot>
    </div>
</template>

在子组件中通过一个内置组件<slot>来接受父组件传递过来的结构,最终完成页面渲染。

7.常见的3种插槽

7.1 默认插槽

我们刚刚书写的插槽比较简单,子组件直接接受父组件传递过来的结构渲染即可,我们把这种称之为默认插槽。

7.2 具名插槽

更多时候,我们希望能控制插槽的一些具体行为。比如在父组件中,希望给子组件传递多个结构,在子组件中需要能在不同试图区域渲染这些结构,如果按照默认插槽的机制,显然无法将每个插槽对应起来,这时我们可以通过给插槽命名的方式来实现,这种插槽称为具名插槽,一起来看这个案例:

html
//子组件中的结构
<div class="container">
  <header>
    <!-- 我们希望把页头放这里 -->
  </header>
  <main>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div>

现在希望将父组件传递过来的结构进行正确地分发,那么在父组件中,通过v-slot这个指令来实现:

html
//父组件中的结构
<Child>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</Child>

同时,在子组件中,通过给<slot>添加name属性来正确地接受父组件的分发:

html
<div class="container">
  <header>
    <!-- 我们希望把页头放这里 -->
     <slot name='header'></slot>
  </header>
  <main>
    <!-- 我们希望把主要内容放这里 -->
      <slot name='default'></slot>
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
      <slot name='footer'></slot>
  </footer>
</div>

其中,默认插槽的v-slot指令和name属性可以省略。

7.3 作用域插槽

插槽的本质仍然是父子组件之间的通信,在父组件向子组件分发插槽的过程中,如果父组件要使用子组件中的数据应该如何来做呢?作用域插槽就是解决了这个问题:

vue
//子组件
<template>
    <div>
        这是子组件
        <slot :msg='msg'></slot>
    </div>
</template>

<script>
export default {
    data() {
        return {
            msg:'子组件中的信息'//父组件会使用到这个msg
        }
    },
}
</script>

<style>
</style>

通过在<slot>上绑定属性的方式,可以将数据传递给父组件。

vue
//父组件
<template>
  <div class="app">
    <Child>
      <template v-slot='msg'>
        <h1>{{msg.msg}}</h1>
      </template>
    </Child>
  </div>
</template>

在父组件中通过v-slot='msg'的形式来接受子组件的数据,msg是可以任意指定的标识符。

作用域也可以理解为子组件向父组件传递数据的一种方式。

版权声明 鄂ICP备2022000216号-2