06 动态组件插槽自定义指令

235 阅读8分钟

1. 动态组件

1.1 什么是动态组件

动态组件指的是动态切换组件的显示与隐藏。

1.2 如何实现动态组件渲染

vue 提供了一个内置的 <component> 组件,专门用来实现动态组件的渲染。示例代码如下:

<template>
  <div class="app-container">
    <h1>App 根组件</h1>
    <hr />
    <button @click="flag = 'Left'">展示左组件</button>
    <button @click="flag = 'Right'">展示右组件</button>
    <!-- component 标签通过 is 属性动态指定要渲染的组件 -->
    <componnet :is="flag"></componnet>
  </div>
</template>

<script>
import Left from "@/components/Left.vue";
import Right from "@/components/Right.vue";

export default {
  components: {
    Left,
    Right,
  },

  data() {
    return {
      flag: "Left",
    };
  },
};
</script>

<componnet :is="flag"></componnet>flag 如果是 Left ,相当于 <Left></Left>

所以 <componnet> 标签相当于就是组件的占位符,is 属性的值就是要渲染的组件名字,即需要在 components 节点下注册的名称。

1.3 切换后无法保持状态

默认情况下,切换动态组件时无法保持组件的状态,例如:将左组件数据加到 5,切换到右组件再切回来,左组件数据又重新回到默认值。此时可以使用 vue 内置的 <keep-alive> 组件保持动态组件的状态。示例代码如下:

<keep-alive>
	<componnet :is="flag"></componnet>
</keep-alive>

切换后无法保持状态的根本原因是:切换后,原来的组件会被销毁(生命周期),切换回来又被重新创建,所以原先的数据就无法保存,而是回到默认值。

<keep-alive> 原理是:将其标签内部的组件进行缓存,跳过了销毁组件这个流程

1.4 keep-alive 对应的生命周期函数

当组件被缓存时,会自动触发组件的 deactivated 生命周期函数。

当组件被激活时,会自动触发组件的 activated 生命周期函数。

我们在左组件写四个生命周期函数:

created() {
  console.log("Left created");
},

destroyed() {
  console.log("Left destroyed");
},

activated() {
  console.log("Left activated");
},

deactivated() {
  console.log("Left deactivated");
},

一开始组件从无到右数据变化会触发 created 生命周期,且该组件会激活,所以触发 activated 生命周期。

在这里插入图片描述

切换到右组件,左组件被缓存,所以触发了 deactivated 生命周期函数,再切换回来,左组件再次被激活,再次触发 activated 生命周期函数。后面就是 activateddeactivated 随着切换,不断反复出现。

在这里插入图片描述

1.5 keep-alive 的 include 属性

include 属性用来指定:只有名称匹配的组件会被缓存。多个组件名之间使用英文的逗号分隔:

<keep-alive include="Left">
	<componnet :is="flag"></componnet>
</keep-alive>

该代码表示只有 Left 组件需要被缓存,没写到的 Right 组件在切换时就会被销毁。

同样与 include 相对的是 exclude,表示只有名称匹配的组件不会被缓存。用法与 include 类似,但两者不能同时存在。

2. 插槽

2.1 什么是插槽

插槽(Slot) 是 vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。

在这里插入图片描述

可以把插槽认为是组件封装期间,为用户预留的内容的占位符。

2.2 插槽的基础用法

在封装组件时,可以通过 <slot> 元素定义插槽,从而为用户预留内容占位符。示例代码如下:

右组件:

<template>
  <div class="right-container">
    <h3>Right 组件</h3>
    
    <!-- 插槽 -->
    <slot></slot>

    <h3>Right 组件底部</h3>
  </div>
</template>

App.vue:

<Right>
	<p>111111</p>
</Right>

最终结果:

在这里插入图片描述

最终 <slot><slot/> 位置被 <p>111111</p> 替换。这就像我们小时候玩游戏的游戏机卡带,如果需要玩超级玛丽需要插个卡带,需要玩魂斗罗又要重新插个卡带。卡带是我们自定义的,游戏机留着这个位置,让我们想玩什么就插入什么卡带。

同理,<slot> 标签相当于游戏机上的卡槽,App.vue 上对应组件内部内容为游戏机的卡带,里面是什么就呈现什么。

2.3 没有预留插槽的内容会被丢弃

如果在封装组件时没有预留任何 <slot> 插槽,则用户提供的任何自定义内容都会被丢弃。将之前 2.2 <slot><slot/> 去掉,会发现 111111 内容不再体现。

很简单的道理,你游戏机卡槽坏了,你有卡带游戏也玩不了。

2.4 后备内容

封装组件时,可以为预留的 <slot> 插槽提供后备内容(默认内容)。如果组件的使用者没有为插槽提供任何内容,则后备内容会生效。示例代码如下:

右组件:

<template>
  <div class="right-container">
    <h3>Right 组件</h3>

    <!-- 插槽 -->
    <slot>
      <h3>我是后备内容</h3>
    </slot>

    <h3>Right 组件底部</h3>
  </div>
</template>

App.vue:

<!-- 没有为插槽提供任何内容 -->
<Right></Right>

最终结果:

在这里插入图片描述

还是用游戏机来举例,游戏机自带一个卡带,如果我们没有使用自己的卡带,那就只能玩它这个自带卡带里的游戏。如果我们使用自己的卡带,那么玩的游戏就是我们自己卡带的。

2.5 具名插槽

如果在封装组件时需要预留多个插槽节点,则需要为每个 <slot> 插槽指定具体的 name 名称。这种带有具体名称的插槽叫做具名插槽。示例代码如下:

右组件:

<template>
  <div class="right-container">
    <h3>Right 组件</h3>

    <!-- 插槽 -->
    <slot name="poemTitle"></slot>
    <slot name="poemContent"></slot>
    <slot></slot>

    <h3>Right 组件底部</h3>
  </div>
</template>

App.vue:

<Right>
  <template v-slot:poemTitle>
    <h3>静夜思</h3>
  </template>

  <template v-slot:poemContent>
    <p>窗前明月光,疑是地上霜。</p>
  </template>

   <template>
      <p>我是 default</p>
    </template>
</Right>

最终:

在这里插入图片描述

注意:

  • 没有指定 name 名称的插槽,会有隐含的名称叫做 default,这也是为什么没有名字的插槽可以渲染到页面上的原因

2.6 v-slot 简写

v-onv-bind 一样,:v-slot 也有缩写,即字符 #

简化前:

<Right>
  <template v-slot:poemTitle>
    <h3>静夜思</h3>
  </template>

  <template v-slot:poemContent>
    <p>窗前明月光,疑是地上霜。</p>
  </template>
</Right>

简化后:

<Right>
  <template #poemTitle>
    <h3>静夜思</h3>
  </template>

  <template #poemContent>
    <p>窗前明月光,疑是地上霜。</p>
  </template>

  <p>我是 default</p>
</Right>

2.7 作用域插槽

在封装组件的过程中,可以为预留的 <slot> 插槽绑定 props 数据,这种带有 props 数据的 <slot> 叫做作用域插槽

子组件 Right.vue

<slot name="message" info="fsda"></slot>

父组件 App.vue

<Right>
  <template #message="scope">
    <p>{{ scope.info }}</p>
  </template>
</Right>

子组件通过插槽的自定义属性可以传值给父组件,父组件在使用插槽时,一般需要指定名字,如上面代码中父组件使用插槽就需要 #message,其值就为子组件插槽的自定义属性的键值对。

如上面代码中,scope 接收的对象为:

{ "info": "fsda" }

不传值默认对象就为 {}

2.8 解构插槽

作用域插槽对外提供的数据对象,可以使用解构赋值简化数据的接收过程。

子组件 Right.vue

<slot name="message" info="abcd" :msg="userinfo"></slot>

export default {
  data() {
    return {
      userinfo: {
        name: "zs",
        age: 20,
      },
    };
  },
}

父组件 App.vue

<Right>
  <template #message="{ info, msg }">
    <p>{{ info }}</p> 
    <p>{{ msg.name }}</p>
    <p>{{ msg.age }}</p>
  </template>
</Right>

通过对键的解构,我们可以直接获取其键的值,然后再进行操作会更加简单。

3. 自定义属性

3.1 什么是自定义指令

vue 官方提供了 v-textv-forv-modelv-if 等常用的指令。除此之外 vue 还允许开发者自定义指令。

3.2 自定义指令的分类

vue 中的自定义指令分为两类,分别是:

  • 私有自定义指令
  • 全局自定义指令

3.3 私有自定义指令

在每个 vue 组件中,可以在 directives 节点下声明私有自定义指令。示例代码如下:

directives: {
  color: {
    // el 代表绑定到的元素,所以 el 是原生的 DOM 对象
    bind(el) {
      el.style.color = "red";  // 绑定到的元素颜色变红
    },
  },
},

使用如下:

<h3 v-color>Right 组件</h3>

注意:

  • 使用时需要以 v- 开头
  • 私有自定义组件只能在自身使用
  • 自定义指令第一次绑定到元素上,会立即触发 bind 函数

3.4 为自定义指令动态绑定参数值

template 结构中使用自定义指令时,可以通过等号 = 的方式,为当前指令动态绑定参数值:

<h3 v-color="color">Right 组件</h3>

data() {
  return {
    color: "green",
  };
},

而自定义指令中需要怎么接收这个值呢?

在声明自定义指令时,可以通过形参中的第二个参数 binding,来接收指令的参数值:

directives: {
  color: {
    // binding 是一个对象,该对象的 value 就是传递过来的值 
    bind(el, binding) {
      el.style.color = binding.value;
    },
  },
},

但是这里有一点要强调,

<h3 v-color="color">Right 组件</h3>

像这句代码,里面接收不是 color 字符串,而是 color 在数据中所代表的值。

所以我们如果要自定义为红色,应该这样写:

<h3 v-color="'red'">Right 组件</h3>

3.5 update 函数

我们定义一个按钮,按了之后可以使得 Right 组件字样从绿色变成红色,但我们发现,只有数据变了,Right 组件这几个字依旧为绿色。

<template>
  <div class="right-container">
    <h3 v-color="color">Right 组件</h3>
    <button @click="color = 'red'">变红</button>
    <h3>Right 组件底部</h3>
  </div>
</template>

<script>
export default {
    data() {
      return {
        color: "green",
      };
    },

    directives: {
      color: {
        // binding 是一个对象,该对象的 value 就是传递过来的值 
        bind(el, binding) {
          el.style.color = binding.value;
        },
      },
    },
}
</script>

这是因为bind 函数只调用 1 次:当指令第一次绑定到元素时调用,当 DOM 更新时 bind 函数不会被触发。

update 函数会在每次 DOM 更新时被调用。示例代码如下:

directives: {
    color: {
      // 第一次绑定元素被调用
      bind(el, binding) {
        console.log(binding);
        el.style.color = binding.value;
      },
      // 每次 DOM 更新被调用
      update(el, binding) {
        console.log(binding);
        el.style.color = binding.value;
      },
    },
  },

3.6 函数简写

如果 bindupdate 函数中的逻辑完全相同,则对象格式的自定义指令可以简写成函数格式:

directives: {
  // 函数名就为自定义指令名,其他与 update 和 bind 函数逻辑一致
  color(el, binding) {
    console.log(binding);
    el.style.color = binding.value;
  },
},

3.7 全局自定义指令

全局共享的自定义指令需要通过 Vue.directive() 进行声明,示例代码在 main.js 里写:

Vue.directive('color', function (el, binding) {
  el.style.color = binding.value;
})

本文主要学习 黑马程序员Vue全套视频教程,从vue2.0到vue3.0一套全覆盖,前端学习核心框架教程

如有错误,敬请指正,欢迎交流🤝,谢谢♪(・ω・)ノ