组件拆分
组件就相当于页面的零件,当做正常的标签使用,不过能够进行自定义的数据传输和事件监听。
组件内也能使用其他的组件,任意处都能够使用。
示例:最简单的组件使用:
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样式设置作用域,可能会影响其他组件的样式。
组件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指定,则会报警告
当指定以后,则会在指定的标签上产生对应的绑定
子传父
上面讲过,父组件向子组件传输是通过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会显得特别的麻烦,这个时候可以通过 provide 和 inject进行操作实现。
父组件在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>
效果
事件总线插件 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>
效果图
动态组件
动态组件,顾名思义就是组件可以不固定写死,能够灵活变通;主要是因为使用了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);
}
},
}
效果图
组件的生命周期
相比于vue2来说,vue3将destroy销毁换成更语义化的unmount卸载,其他并无区别
动态组件的<keep-alive></keep-alive>第一次进入会进入生命周期,其他都是会在自身的生命周期
- 激活 activated
- 失活 deactivated
在父子组件中的钩子函数调用顺序
在父组件挂载前到挂载结束这一期间, 子组件完成所有操作。
完整的生命周期
// 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
}
}
},
};