返回

Vue 组件

Table of contents

Open Table of contents

Vue 的组件化

由于组件化特殊,需采用 vue cli 创建项目。如何安装 Vue cli 请见初识 Vue3

注册组件

注册组件分成两种:

全局组件

注册全局组件的流程:

  1. 创建全局组件进行编写
  2. main.js引入并注册该组件
  3. 在其它组件中使用
    • 代码
    • 结果

局部组件

通常使用组件的时候采用的都是局部注册,因为全局组件就算有些没有被使用,在打包时也会被一起打包,增加包的大小。

<!-- ComponentA 组件 -->
<template>
  <h2>这是局部组件 A</h2>
</template>

<!-- ComponentB 组件 -->
<template>
  <h2>这是局部组件 B</h2>
</template>

<!-- 组件 C 使用 A 和 B 两个组件 -->
<template>
  <h1>这个是App组件</h1>
  <ComponentA />
  <ComponentB />
</template>

<script>
  // 引入组件
  import ComponentA from "@/components/ComponentA.vue";
  import ComponentB from "@/components/ComponentB.vue";
  export default {
    name: "App",
    // 注册局部组件
    components: {
      // 这里是 ComponentA:ComponentA 的简写
      ComponentA,
      ComponentB,
    },
  };
</script>

MyComponent 注册后,在 template 中使用时可以写作<my-component /><MyComponent />

[!WARNING] 局部引入的组件,一定要在components 方法中进行注册才能使用。components内部是以[key: string]: value的格式书写

配置路径别名

当我们想要引用一个嵌套极深的文件时。需要../../../../utils/test,这对于开发是很友好的。 vue.config.js,该文件会和webpack的默认配置进行合并,从而实现对webpack的修改。而我们想要解决路径嵌套的问题,首先需要在webpack中进行配置。 第一步:vue.config.js中写入configureWebpack进行修改默认配置

const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
  transpileDependencies: true,
  configureWebpack: {
    resolve: {
      // 配置路径别名
      // @ 是内置已经配置好的路径别名:对应的是 src 的路径
      alias: {
        utils: "@/utils",
      },
    },
  },
});

当我们配置完之后,就可以使用import {} from 'utils/test'进行直接使用。虽然配置生效了,但是 Vscode 是没有智能提示的,原因是 Vscode 并不知道配置了该路径别名。 第二步:js.config.json中进行配置。js.config.json是针对于 Vscode 的文件,以便给与更友好的提示

{
  "compilerOptions": {
    // ...
    "paths": {
      "@/*": ["src/*"],
      "utils/*": ["src/utils"]
    }
    // ...
  }
}

组件通信

父子组件通信

父子组件之间进行通信的两种方式:

  1. 父组件传递给子组件:通过props属性。父传子
  2. 子组件传递给父组件:通过$emit触发事件。子传父

父传子

组件最大的优势就是可复用性,不同的信息需要相似的结构去展示时,就可以将相似的结构封装为一个组件。并动态展示不同的数据信息。

props 接收
  1. 父组件传递给子组件
<template>
  <h1>这里是父组件</h1>
  <InfoItem name="张三" sex="" age="18" />
  <InfoItem name="李四" sex="" age="20" />
  <InfoItem />
</template>
<script>
  import InfoItem from "./components/InfoItem.vue";
  export default {
    name: "App",
    components: {
      InfoItem,
    },
  };
</script>
  1. 子组件接收数据
<template>
    <ul>
        <li>姓名:{{ name }}</li>
        <li>性别:{{ sex }}</li>
        <li>年龄:{{ age }}</li>
    </ul>
</template>

<script>
export default {// 必须通过 props 接收
    props: ['name', 'sex', 'age']
}
props 的接收方式

Props 有两种常见的用法:

数组写法

数组类型只能接收,不能对接收到的值进行限制,比如,要求传入的值必须是 Array,必须为 Number 类型

export default {
  props: ["name", "sex", "age"],
};
对象写法

常用写法:

export default {
  props: {
    // 限制 name 为 字符串,若不传 name,则显示 默认值 default
    name: {
      type: String,
      default: "我是默认Name",
    },
    // sex 属性为必传项
    sex: {
      type: String,
      default: "人妖",
      required: true,
    },
    // 仅仅限制 age 为 数字类型
    age: Number,
  },
};

当子组件要求age字段必须为数字时,父组件的<InfoItem age="18" />就需要改为<InfoItem :age="18" />

props 对象写法拓展
非 props 的 Attribute(了解)
  1. 什么是非 Prop 的 Attribute 呢?当我们传递给一个组件某个属性,但是该属性并没有定义对应的 props 或者 emits,就称之为 非 Prop 的 Attribute;常见的包括 class、style、id 属性等;

    结果

  2. 禁用 Attribute 继承禁止非 props 的 attribute 添加到子组件根节点上,可以在使用inheritAttrs: false来禁止

    export default {
      inheritAttrs: false,
      // props: ["name", "sex", "age"]
    };
  3. 指定某个节点继承attribute 若要指定某个节点继承父组件的非 props 的 attribute,可以使用 $attrs 来访问所有的 非 props 的 attribute 属性

    <!-- <h2 age="18"></h2> -->
    <h2 :age="$attrs.age"></h2>
    <!-- <h2 name="张三"></h2> -->
    <h2 :name="$attrs.name"></h2>
    <!-- <h2 class="name age sex"></h2> -->
    <h2 :class="$attrs"></h2>

子传父

子组件有一些事件触发,比如子组件中发生了点击,父组件需要切换不同的内容,或者子组件有一些内容需要传递给父组件时,使用子传父。

$emits 使用

**案例:**父组件显示counter值,子组件内部的按钮点击时,父组件需要更新counter值 **思路:**给按钮绑定点击事件,当点击时,我们需要去通知父组件更改counter的值,父组件在得到通知后,需要做出对应的操作去完成此次响应。

  1. 给按钮绑定点击事件@click="btnClick(1),当点击时,我们可以拿到点击需要加减的值count
  2. 拿到之后,我们需要把这个值告诉父组件,让其加减这个值count,使用$emit $emit可以接收两个参数,(参数 1:自定义事件,参数 2:传递给父组件的值)
<!-- AddCount.vue -->
<template>
  <div class="add">
    <h4>Add子组件</h4>
    <button @click="btnClick(1)">点击+1</button>
    <button @click="btnClick(5)">点击+5</button>
    <button @click="btnClick(10)">点击+10</button>
  </div>
</template>

<script>
  export default {
    methods: {
      btnClick(count) {
        // 让子组件发出一个自定义事件
        // 参数1:自定义的事件名称,参数2:传递的参数
        // 当触发 addClick 方法时,addClick会得到 count
        this.$emit("addClick", count);
      },
    },
  };
</script>
  1. 父组件需要在子组件上绑定自定义事件,即$emit的第一个参数addClick,书写格式和@click=""基本一致
  2. 绑定完成后,等号右侧就和@click一样,是一个需要触发的事件handleClick,毕竟子组件通知父组件后,父组件需要做出相应的操作
  3. handleClick会默认得到子组件传递过来的count,可以直接拿到count值,此时就可以进行加减操作
<!-- App.vue -->
<template>
  <div class="parent">
    <h1>父组件Count:{{ counter }}</h1>
    <!-- addClick 就是 $emit 中的第一个参数 handleClick 就是 需要做出操作的函数 -->
    <AddCount @addClick="handleClick" />
  </div>
</template>

<script>
  import AddCount from "./components/AddCount.vue";

  export default {
    name: "App",
    data() {
      return {
        counter: 0,
      };
    },
    components: { AddCount },
    methods: {
      // 父组件接收子组件传递过来的数据,并作出操作完成此次通知
      handleClick(count) {
        console.log("子组件传递过来的count", count);
        this.counter += count;
      },
    },
  };
</script>

内部其实是监听按钮的点击,点击之后通过 this.$emit 的方式发出去事件

[!ATTENTION] 切勿通过 props 将 count 值传递给子组件,让子组件进行修改,这违反了单向数据流原则

自定义事件的参数和验证(了解)

emits是在 Vue3 中新增的,我们可以把所有需要通知的事件写在emits中,可以提高代码的可读性,在 Vscode 中在子组件标签上写这些自定义事件会有代码提示

export default {
  // 1、数组语法
  // emits: ["addClick"],
  // 2、对象语法,可以对参数进行验证
  emits: {
    addClick: function (count) {
      if (count <= 5) return true;
      return false;
    },
  },
  methods: {
    btnClick(count) {
      this.$emit("addClick", count);
    },
  },
};

当自定义验证生效时,控制台会报出警告,说是一个校验失败的自定义事件

自定义事件校验失败

非父子组件通信

在开发中,我们构建了组件树之后,除了父子组件之间的通信之外,还会有非父子组件之间的通信。传递的层级过深时,使用ProvideInject

Provide Inject

基本使用:

provide inject流程

Provide 函数写法

如果Provide中提供的一些数据是来自 data,那么我们可能会想要通过 this 来获取

函数写法

provide 一般都写成函数写法

Provide 响应式数据

当我们使用 provide 给子组件传递数据时,我们更希望该数据是响应式的

非响应式provide数据

解决方法:使用computed

computed响应式数据

computedvue3 的新特性,且computed返回的是一个ref对象,需要取出其中的value来使用

事件总线

在 Vue2 中,官方内置了eventbus事件总线,但 Vue3 从实例中移除了 $on、$off、 $once 方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库:

Vue2 全局事件总线
  1. 初始化

    // main.js
    // 入口文件加载时,直接在 VueComponent.propotype 上添加 $bus 实例
    // 为了方便起见,可以把 EventBus 实例化为 $bus
    var EventBus = new Vue();
    
    Object.defineProperties(Vue.prototype, {
      $bus: {
        get: function () {
          return EventBus;
        },
      },
    });
  2. 发送事件 A 页面点击,通知 B 页面

    <!-- A.vue -->
    <template>
      <button @click="sendMsg()">-</button>
    </template>
    <script>
      export default {
        methods: {
          sendMsg() {
            this.$bus.$emit("toBMsg", "来自A页面的消息");
          },
        },
      };
    </script>
  3. 接收事件

    <!-- B.vue -->
    <template>
      <p>{{ msg }}</p>
    </template>
    <script>
      export default {
        data() {
          return {
            msg: null,
          };
        },
        mounted() {
          // mounted 页面挂载完成就开始监听事件
          // 监听的事件触发,则会接收到 发送过来的 数据
          ths.$bus.$on("toBMsg", (msg) => {
            // A发送来的消息
            this.msg = msg;
          });
        },
      };
    </script>
  4. 移除事件监听通常在当前页面销毁时,移除该页面的事件监听this.$bus.$off('aMsg')

插槽(slot)

Vue 中将 <slot> 元素作为承载分发内容的出口;在封装组件中,使用 特殊的元素 <slot> 就可以为封装组件开启一个插槽;该插槽插入什么内容取决于父组件如何使用;

使用插槽

基本使用

slot基本使用

默认插槽内容

默认插槽内容

具名插槽

当有一个组件有多个插槽时,为了防止插槽使用混乱,需要对其进行命名

具名插槽

最常用的是第三种写法

动态插槽名

我们可以通过 v-slot:[dynamicSlotName] 方式动态绑定一个名称

动态插槽名

插槽难点

认识渲染作用域
认识作用域插槽

为什么要使用作用域插槽?举例:

  1. 当子组件会接收list 数组进行循环时,如果使用插槽,内部的循环显示的标签是固定的,
<template>
  <div class="nav-bar">
    <template v-for="item in list" :key="item.id">
      <div class="item">
        <!-- 正常写法 -->
        <span>{{ item.title }}</span>
        <!-- 插槽写法 -->
        <!-- 这种写法是不成功的,父组件插入插槽中的内容会循环,导致的结果就是每一个 item 的内容一样 -->
        <!-- 原因是:这里的 item 父组件是访问不到的,父组件传递过来的内容,会直接把 slot 的默认内容(循环项)替换掉 -->
        <!-- <slot><span>{{ item.title }}</span></slot> -->
      </div>
    </template>
  </div>
</template>

结果

提问: 在这种情况下,该如何在子组件有插槽的循环中插入循环项的自定义内容 思路: 将插槽的当前项传递给父组件,供父组件访问

  1. slot 标签绑定自定义attribute当前循环项插槽的数据传递给父组件,父组件通过v-slot:default="xxx"(简写#default="xxx")接收,xxx代表的是传递给父组件的所有数据,所以是一个对象,可以通过xxx.id、xxx.title的格式访问这些数据。

作用域插槽

认识独占默认插槽的缩写

生命周期

生命周期流程图

  1. 创建(组件实例初始化)阶段
    1. beforeCreate:开始创建组件实例
    2. created(重要):组件实例创建完成。发送网络请求、事件监听、this.$watch
  2. 挂载(template 模板编译,数据渲染)阶段
    1. beforeMount:DOM 开始挂载
    2. mounted(重要):元素已被挂载。获取 DOM、使用 DOM
  3. 更新(data 数据变化更新 el 节点)阶段
    1. beforeUpdate:根据最新数据,预生成新的 VNode
    2. update:新的真实 DOM 已经更新完毕
  4. 销毁阶段
    1. beforeUnmout:组件预卸载
    2. unmounted(相对重要):组件实例销毁。回收操作、取消事件监听、定时器等

多组件 lifecycle 执行顺序:

父子组件挂载阶段

父子组件挂载阶段

父子组件更新阶段

父子组件更新阶段

父子组件卸载阶段

父子组件卸载阶段

$refs

  1. 标签或组件内部通过写入ref="xxx"绑定ref
  2. 通过this.$refs.xxx访问到对应的原生 DOM 节点
<template>
  <!-- ref 绑定元素,获取到的是原生DOM节点 -->
  <h1 ref="titleRef">{{ title }}</h1>

  <!-- ref 绑定组件,获取到的是组件实例 -->
  <MyRef ref="componentRef" />

  <button @click="btnClick">点击</button>
</template>

<script>
  import MyRef from "./components/MyRef.vue";

  export default {
    name: "App",
    data() {
      return {
        title: "Hello World",
      };
    },
    components: { MyRef },

    methods: {
      btnClick() {
        // 获取到 元素节点,可以通过 .style、.innerHTML 等操作原生 DOM 的方式进行操作
        console.log(this.$refs.titleRef);

        // this.$refs.componentRef 就是组件实例,即 组件的 this
        console.log(this.$refs.componentRef);
        // 所以可以直接访问 data 中的数据
        console.log(this.$refs.componentRef.message);
        // 绑定组件可以通过 this.$refs.componentRef.$el 拿到组件的根元素
        console.log(this.$refs.componentRef.$el);
        // 若组件由多个根节点,获取到的是第一个 node 节点

        // console.log(this.$parent)   // 访问当前组件的父组件
        // console.log(this.$root)    // 获取根组件
        // Vue3 中移除了 $children
      },
    },
  };
</script>

<style></style>

结果

ref 绑定元素时,this.$refs.xxx 获取到的是 DOM 节点。当 ref 绑定 组件时,this.$ref.xxx 获取的是该组件的组件实例,可以直接访问内部的数据(data 中的数据,methods 中的方法等所有能够通过 this 访问的

component 动态组件

基本使用

当我们需要动态显示组件(组件的切换)时,首先想到的就是v-if,这种当组件判断过多时会显得臃肿,可以尝试第二种component动态组件标签

component标签内部通过特殊的attributeis,来动态展示组件,is内部就是需要显示的组件名称,前提是组件必须是全局组件或已被注册的局部组件

<template>
  <div id="app">
    <div class="navbar">
      <template v-for="(item, index) in nav" :key="index">
        <div
          :class="{ active: currntIndex === index }"
          @click="btnClick(index)"
        >
          {{ item.title }}
        </div>
      </template>
    </div>
    <!-- 方式一:普通写法,通过 v-if 判断当前需要显示的组件 -->
    <MyHome v-if="currntIndex === 0" />
    <MyAbout v-else-if="currntIndex === 1" />
    <MyCenter v-else />
    <!-- 方式二:component 动态组件 -->
    <component :is="nav[currntIndex].component" />
  </div>
</template>

<script>
  import MyAbout from "./components/MyAbout.vue";
  import MyCenter from "./components/MyCenter.vue";
  import MyHome from "./components/MyHome.vue";

  export default {
    name: "App",
    components: { MyHome, MyAbout, MyCenter },
    data() {
      return {
        nav: [
          { title: "首页", component: "MyHome" },
          { title: "中心", component: "MyCenter" },
          { title: "关于", component: "MyAbout" },
        ],
        currntIndex: 0,
      };
    },
    methods: {
      btnClick(index) {
        this.currntIndex = index;
      },
    },
  };
</script>

动态组件传值

组件通信,可以直接在 <component />标签中传值,此时component标签可以当做组件进行通信

<component :is="nav[currntIndex].component" @sendMsg="getMsg" :msg="msg" />

keep-alive

缓存组件

在切换组件后,切换前的组件会被销毁掉,再次回来时会重新创建组件,所有的元素及状态都会重新编译,若需要在切换组件时依旧让组件不被销毁,可以使用keep-alive

  1. keep-alive 内部包裹的组件默认都会被缓存

    <KeepAlive>
      <!-- 这里的组件当切换时,不会销毁和重新创建 -->
      <component :is="nav[currntIndex].component" />
    </KeepAlive>
  2. include 只有名称匹配的组件会被缓存,写法:string | RegExp | Array<string>

    include写法

  3. exclude 任何名称匹配的组件都不会被缓存,用法和 include一致

  4. 最多可以缓存多少组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁。写法:number | string

缓存生命周期

异步组件

对于某些组件若需要通过异步的方式来进行加载(目的是可以对其进行分包处理),Vue 中提供了一个函数:defineAsyncComponent

组件的 v-model

在 input 中可以使用v-model来完成双向绑定。vue 也支持在组件上使用v-model

基本使用

<MyHome v-model="message" />
<!-- 等价于 -->
<MyHome :modelValue="message" @update:modelValue="message = $event" />

组件 v-model 实现

组件v-model实现

多个 v-model 绑定

<UserName v-model:firstName="first" v-model:lastName="last" />
<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>
<script>
  export default {
    props: ["firstName","lastName"]
    emits: ["update:firstName", "update:lastName"],
  };
</script>

Mixin

基本使用

mixins基本使用

Mixin 的合并规则

mixins对象中的选项组件对象中的选项发生了冲突,Vue 分成不同的情况来进行处理

全局混入 mixin

组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的 mixin

const app = createApp(App);
app.mixin({
  create() {
    console.log("global mixin created");
  },
});

全局混入,一旦注册,将会影响到所有的组件



上一篇
Vue3 组合式 API
下一篇
初识 Vue3