Vue3组件的使用

6,989 阅读1分钟

代码示例,可参考

组件拆分

组件就相当于页面的零件,当做正常的标签使用,不过能够进行自定义的数据传输和事件监听。

组件内也能使用其他的组件,任意处都能够使用。

示例:最简单的组件使用:

App.vue

<template>
  <div>
    <Header></Header>
  </div>
</template>
<script>
import Header from "./Header.vue";
</script>

Hearder.vue

<template>
  <div>
    <h1>header</h1>
    <Tabbar></Tabbar>
  </div>
</template><script>
import Tabbar from "./Tabbar.vue";
export default {
  components: { Tabbar },
};
</script>

Tabbar.vue

<template>
  <div>
    Tabbar
  </div>
</template>

CSS样式作用域

在style标签上写上scoped属性,能够将写的CSS样式局限在该组件中。

vue文件在编译后,会有自身的标识,同样的文件内写的css也会有,所以当组件相互使用的时候不将css样式设置作用域,可能会影响其他组件的样式。

image-20220110091007553.png

组件props

子组件可以获取父组件传输,绑定在props声明的属性上,子组件能够直接使用

未绑定在props属性,默认会传输到子组件的根节点上,也可以通过使用模板语法搭配 $attrs属性进行绑定

父组件:App.vue

<template>
  <div class="container">
    <!-- 传输未定义的props,会在根节点进行补充 -->
    <Header class="why" :title="msg" parent="app" name="header"></Header>
    <!-- 当存在多个根节点时,会报警告无法识别放在那个根节点上 -->
    <Root class="haha"></Root>
  </div>
</template>

Header.vue

<template>
   <!-- 单独的根组件 -->
  <div>
    <h1>{{ title }}</h1>
    <!-- 也可以通过 $attrs属性进行查询 -->
    <h1 :name="$attrs.name">{{ parent }}</h1>
  </div>
</template><script>
export default {
  // 指定传入进来的属性的类型
  props: {
    title: String,
    // 对象形式
    parent: {
      type: String,
      // 必须
      required: true,
      // 默认值
      default: () => {
        return "hahah";
      },
    },
  },
};
</script>

RootComponent.vue

<template>
  <h1>RootComponent</h1>
  <h1 :name="$attrs.name">RootComponent</h1>
  <!-- 多个根节点需要指定,同时直接绑定$attrs可以解构 -->
  <h1 v-bind="$attrs">RootComponent</h1>
</template> 

多根节点的时候如果没有指定$attrs指定,则会报警告

image-20220110092545717.png 当指定以后,则会在指定的标签上产生对应的绑定

image-20220110092621572.png

子传父

上面讲过,父组件向子组件传输是通过props属性绑定

父子组件是单向数据流,只能一方向另一方传输;子组件给父组件传值通过 this.$emit("函数名", 值);

子组件的事件只能在子组件内部使用,如果父组件想要使用子组件中的事件来实现自己的行为,也需要this.$emit进行绑定

父组件

<template>
  <div>
    <h2>当前计数:{{ count }}</h2>
     <!-- 使用子组件的点击事件 -->
    <child-btn @add="increment" @sub="decrement"></child-btn>
  </div>
</template><script>
import ChildBtn from "./ChildBtn.vue";
export default {
  components: {
    ChildBtn,
  },
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement(obj) {
      // 可以接收子组件传输来的值
      console.log(obj);
      this.count--;
    },
  },
};
</script>

子组件

<template>
  <div>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template><script>
  export default {
    // emits是声明组件传输定义的函数名
    // emits: ["add", "sub"],
    // 对象的写法是对参数的验证
    emits: {
      add: null,
      sub: (payload) => {
        // 拦截
        console.dir(payload);
        if(payload.age >= 18) return false
        else return true
      }
    },
    methods: {
      increment() {
        this.$emit("add")
      },
      decrement() {
        let params = {
          name: "lsf",
          age: 18
        }
        // 可以传值
        this.$emit("sub", params)
      }
    },
  }
</script>

父组件给后代传值

有了组件嵌套,但是父组件给孙子组件甚至更后代的组件,仅通过props会显得特别的麻烦,这个时候可以通过 provideinject进行操作实现。

父组件在provide中添加想要传输的属性,在后代组件中能够通过inject接收对应属性键当做自己的属性使用 注意:只能在后代组件中使用

父组件

export default {
  components: { Home },
  // 提供给子孙使用,本身和兄弟或其他组件不能使用
  // 将属性写成函数形式,能够每次返回都是个新的对象:参考vue2的data
  provide() {
    return {
      name: "刘德华",
      age: 18,
      // 将length变成响应式,computed返回一个ref对象
      length: computed(() => this.names.length),
    };
  },
  data() {
    return {
      names: ["bac", "SDf", "Sfr"],
    };
  },
  methods: {
    change() {
      this.names.push('hha')
    }
  },
};

孙子组件

<template>
  <div>HomeContent: {{ name }} - {{ age }} -- {{length}}</div>
</template><script>
export default {
  inject: ["name", "age", "length"],
};
</script>

效果

image-20220110101655209.png

事件总线插件 mitt

有了父组件给孙子组件传输,但是兄弟组件或者其他没有关系的组件,无法进行自定义的数据交互(vuex是存储状态),这时就需要一个事件总线。

在vue2中使用eventBus,而在vue3中删除了该api

npm install mitt 下载插件

全局中导入,使用同一个mitt对象:

  • 在发送事件的组件中使用 emitter.emit("fnName", 值)
  • 接收事件的组件中使用 emitter.on("fnName", (type,info) => {}); fnName如果是*,则匹配所有传来的事件:type是函数名,info是信息
  • 取消事件:emitter.off(fnName)

mitt的使用:新建一个js文件,导出该对象

import mitt from 'mitt'const emitter = mitt()
​
​
// 定义一个函数,用来取消函数监听
emitter.cancelFn = (fnName) => {
  emitter.off(fnName)
}
export default emitter

事件的发送

import emitter from "./utils/eventBus";   
change() {
      console.log("btn点击");
      emitter.emit("foo", {msg: "mitt事件"})
​
      emitter.emit("fn", {name: "fn"})
    }

事件的接受

import emitter from "./utils/eventBus";
export default {
  created() {
    emitter.on("foo", (info) => {
      console.log(info);
    })
​
    emitter.on("*", (type, info) => {
      console.log("监听所有事件:", type, info);
    })
  },
};

插槽的使用

插槽能够让组件充分利用,不仅能使用组件自带的内容,还可以在父组件中diy自定义的内容,让组件复用更灵活

插槽组件:navbar

<template>
  <div class="navbar">
    <div class="left">
      <!-- 具名插槽 --> 
      <slot name="left">左边</slot>
    </div>
    <div class="center">
        <!-- 默认插槽 -->
      <slot>中间默认</slot>
    </div>
    <div class="right">
      <slot name="right">右边</slot>
    </div>
  </div>
  <!-- 名字不固定的插槽,通过变量来定义 -->
  <div class="namebox">
    <slot :name="name"></slot>
  </div>
</template><script>
export default {
  props: {
    name: String
  },
  data () {
    return {
      title: "navbar的title"
    }
  }
}
</script>

父组件使用插槽

    <!-- name需要传入,子组件才能够匹配对应插槽 -->
    <nav-bar :name="name">
      <template v-slot:right>
        <div>
          <h3>替换右边的</h3>
        </div>
      </template>
      <template v-slot:left>
        <h2>替换左边的</h2>
      </template>
      <!-- v-slot:可以缩写成 # -->
      <template #default>
        <div>替换中间默认的</div>
      </template>
      <!-- 加个[]能够插入值,替换想进入的插槽 -->
      <template #[name]>
        <div>未定义name的插槽</div>
      </template>
    </nav-bar>

插槽作用域

插槽作用域:组件是在父组件中编译定义的,数据无法使用所处组件中的数据

想获取本组件中的数据需要在组件中绑定,然后在插槽中获取到

插槽组件

<template>
  <div>
    <template v-for="item,index in names" :key="item">
      <slot :item="item" :index="index"><span>{{item}}</span> |</slot>
    </template>
  </div>
</template><script>
  export default {
    props: {
      names: {
        type: Array,
        default: () => []
      }
    }
  }
</script>

父组件使用插槽

    <data-content :names="people">
      <template v-slot="slotProps">
        <li>{{ slotProps.index + 1 }} --- {{ slotProps.item }}</li>
      </template>
    </data-content>
    <hr />
    <!-- 独占默认插槽缩写:组件中有且只有一个插槽,并且还是默认插槽时使用 -->
    <data-content :names="people" v-slot="slotProps">
      <li>{{ slotProps.index + 1 }} --- {{ slotProps.item }}</li>
    </data-content>
  </div>

效果图

image-20220110104313985.png

动态组件

动态组件,顾名思义就是组件可以不固定写死,能够灵活变通;主要是因为使用了vue的内置组价 <component :is="name"></component> 其中的name就是组件自定义的名字

使用动态组件两种办法

  • v-if判断:使用template包裹组件进行if else判断

        <!-- 1. v-if 判断实现 -->
        <template v-if="currentTab === 'home'">
          <home></home>
        </template>
        <template v-else-if="currentTab === 'about'">
          <about></about>
        </template>
        <template v-else>
          <category></category>
        </template>
    
  • (推荐) 使用内置组件 component:component和使用组件无异,可以正常的传值和绑定事件。

    注意:is后面的值需要在components对象中自定义的写入

          <component
            :is="currentTab"
            name="coder"
            :age="18"
            @pageClick="pageClick"
          ></component>
    
    export default {
      components: {
        home: Home,
        About: About,
        category: Category,
      },
      data() {
        return {
          tabs: ["home", "about", "category"],
          currentTab: "home",
        };
      },
      methods: {
        itemClick(item) {
          this.currentTab = item;
        },
        pageClick() {},
      },
    };
    

keep-alive 组件

keep-alive组件能够将使用过的组件缓存,不会进行销毁重建的操作,保留组件之前的行为。

例如:在组件内定义一个变量num=0,进行操作后 num变成了 8;如果没有使用keepalive缓存,则再次进入组件num值依旧为0,相当于进入组件执行了刷新的操作。

组件使用keepalive

    <!-- keepalive -->
    <!-- include 包括将被缓存的组件,内部变量是组件的name值(和data同级的属性) -->
    <!-- exclude 不被缓存的组件内 -->
    <keep-alive :include="['home']">
      <component
        :is="currentTab"
        name="coder"
        :age="18"
        @pageClick="pageClick"
      ></component>
    </keep-alive>

使用了keepalive的组件,不会进行销毁重建的生命周期,它拥有自己的激活钩子函数

动态组件的<keep-alive></keep-alive>第一次进入会进入创建的周期,其他都是会在自身的生命周期,或更新时调用 update的钩子函数

  • 激活 activated
  • 失活 deactivated

异步组件

利用import函数是的webpack打包时,将函数导出的文件不和app.js文件混合,另外新建一个文件,当使用时再引入,因此当函数引入组件时,可以利用异步组件<suspense></suspense> 以及Vue3内置api defineAsyncComponent配合使用,达到一个很好地交互效果。

异步组件的使用

<template>
  <div>
    异步组件
    <suspense>
      <!-- 加载完毕显示的组件 -->
      <template #default>
        <async-category></async-category>
      </template>
      <!-- 默认组件未加载,占位组件 -->
      <template #fallback>
        <loading></loading>
      </template>
    </suspense>
  </div>
</template>
<script>
// vue3提供的加载异步组件的函数,接收一个方法,返回一个promise
import { defineAsyncComponent } from "vue";
// import AsyncCategory from './AsyncCategory.vue';
// import方法,会让该组件延迟加载,不会统一打包在app.js文件中,而是使用的时候引入进来
// 写法一:常用写法,接受一个函数
const AsyncCategory = defineAsyncComponent(() => import("./AsyncCategory.vue"))
// 写法二:接受一个对象
// const AsyncCategory = defineAsyncComponent({
//   loadingComponent: Loading, // 当异步组件未加载的时候显示该组件
//   loader: () => import("./AsyncCategory.vue"),
//   // errorComponent, // 出错时显示的组件
//   delay: 2000, // 在显示loadingComponent组件之前,等待多长时间
//   /**
//    * err: 错误信息
//    * retry: 函数,调用retry尝试重新加载
//    * fail: 函数,提示加载程序结束退出
//    * attempts:记录尝试的次数
//    */
//   onError: function (err, retry, fail, attempts) {},
// });import Loading from "./Loading.vue";
​
export default {
  components: {
    AsyncCategory,
    Loading,
  },
};

模拟异步:AsyncCategory.vue通过setTimeout函数

<template>
  <div>
    asyncCategory --- {{ten}}
  </div>
</template><script setup>
import {ref} from "vue"
function awaitMe () {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10)
    }, 3000)
  })
}
const num = await awaitMe()
let ten = ref(num)
</script>

获取dom元素或指定组件

通过给dom元素或组件设置属性ref,指定对应的标识,在其他地方能够通过 this.$refs获取到对应的dom元素或组件,并能够使用组件内部的属性和方法

给组件和dom指定ref的标识

<template>
  <div>
    <nav-bar ref="bar"></nav-bar>
    <h2 ref="title">哈哈哈</h2>
    <button @click="btnClick">获取元素</button>
  </div>
</template>
<script>
import NavBar from "./NavBar.vue";
export default {
  components: { NavBar },
  data() {
    return {
      message: "我是bar的父组件",
    };
  },
  methods: {
    btnClick() {
      // this.$refs 是一个proxy对象,存储不同的信息(DOM,组件等)
      console.log(this.$refs);
      // 获取到组件中的title值,还可以调用组件中的函数
      console.log(this.$refs.bar.title);
      this.$refs.bar.sayHello();
      this.$refs.bar.getParentAndRoot();
    },
  },
};
</script>

组件内部定义函数和变量

export default {
  props: {
    name: String
  },
  data () {
    return {
      title: "navbar的title"
    }
  },
  methods: {
    sayHello() {
      console.log("hello 我是bar组件");
    },
    getParentAndRoot() {
      // 获取父组件
      console.log(this.$parent.message);
      // 获取根元素
      console.log(this.$root);
    }
  },
}

效果图

image-20220110110555263.png

组件的生命周期

相比于vue2来说,vue3将destroy销毁换成更语义化的unmount卸载,其他并无区别

动态组件的<keep-alive></keep-alive>第一次进入会进入生命周期,其他都是会在自身的生命周期

  • 激活 activated
  • 失活 deactivated

在父子组件中的钩子函数调用顺序

在父组件挂载前到挂载结束这一期间, 子组件完成所有操作。

image-20220110110802403.png

完整的生命周期

  // keepalive钩子函数
  activated() {
    console.log("home 活跃状态");
  },
  deactivated() {
    console.log("home 变成非活跃状态");
  },
​
  // 正常的生命周期
  beforeCreate() {
    console.log("home beforeCreated");
  },
  created() {
    console.log("home created");
  },
  beforeMount() {
    console.log("home beforeMount");
  },
  mounted() {
    console.log("home mounted");
  },
  beforeUpdate() {
    console.log("home beforeUpdate");
  },
  updated() {
    console.log("home updated");
  },
  // vue3 将 销毁 destroy换成了卸载
  beforeUnmount() {
    console.log("home beforeUnmount");
  },
  unmounted() {
    console.log("home beforeUnmount");
  },

组件的v-model

v-model 就是一个语法糖,vue中帮我们将绑定属性和方法使用v-model指令完成了;而其中内部做的事情需要我们探究一下,能够更好的使用。

v-model语法糖

    <!-- <input v-model="message"> -->
    <!-- 语法糖做的事情 -->
    <!-- <input :value="message" @input="message = $event.target.value"> -->
    
    <!-- 在组件上使用v-model -->
    <!-- 绑定单个 v-model -->
    <!-- <my-input v-model="message"></my-input> -->
    <my-input v-model="message" v-model:title="title"></my-input>
    <!-- vue3内部帮忙做的事情 -->
    <!-- <my-input :modelValue="message" @update:modelValue="message = $event"></my-input> -->

组件内配合v-model,需要做的事情

export default {
  // 不建议直接绑定到props里面的属性,使用computed最好
  props: ["modelValue", "title"],
  emits: ["update:modelValue", "update:title"],
  computed: {
    inputValue: {
      set(value) {
        this.$emit("update:modelValue", value);
      },
      get() {
        return this.modelValue;
      },
    },
    myTitle: {
      set(value) {
        this.$emit("update:title", value);
​
      },
      get() {
        return this.title
      }
    }
  },
};