组件化的优点
组件的拆分
组件的通信
在开发过程中,因为组件化的原因,数据在各个组件之中,而有些数据可能是需要给多个组件使用的,也就是说一个组件修改了数据需要在其他位置展示的数据同时做出改变,这就需要用到组件通信了。
父子组件之间通信的方式
父子组件之间如何进行通信呢?
什么是Props呢?
- Props是你可以在组件上注册一些自定义的attribute
- 父组件给这些attribute赋值,子组件通过attribute的名称获取到对应的值
Props有两种常见的用法:
- 方式一:字符串数组,数组中的字符串就是attribute的名称
方式二:对象类型,对象类型我们可以在指定attribute名称的同时,指定它需要传递的类型、是否是必须的、默认值等等
props为对象类型type取值
type可以是以下的类型:
String
- Number
- Boolean
- Array
- Object
- Date
- Function
-
对象类型的其他写法
props: {
// 直接指定props类型
propA: String,
// 允许props类型为String或Number
propB: { String, Number },
// 除了指定类型,还可以指定是否为必传参数
propC: {
type: String,
required: true
},
// 可以指定默认值
propD: {
type: Number,
dafault: 100
},
// 引用类型默认值使用函数返回,避免直接使用引用类型导致的问题(无法复用)
propE: {
type: Object,
default() {
return { message: "hello" }
}
},
// 可以自定义验证函数
propF: {
validator(value) {
return ['success','warning','danger'].includes(value)
}
},
// 可以传递函数,不过一般用不到
propG: {
type: Function,
default() {
return 'Default function'
}
}
}
注意:如果是引用类型,也就是对象或者数组值的话,默认值最好返回一个函数,因为如果props有复用的情况,引用类型会出现问题。
props大小写命名
HTML中的attribute是大小写不敏感的,所以浏览器会把所有大写字符解释成小写字符
这意味着在使用template模板时,如果是驼峰式命名比如className,最好写成class-name的形式,这也是官方推荐的写法,不过即使写驼峰式Vue也是会识别出来的。非Prop的Attribute
什么是非Prop的Attribute?
当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为非Prop的Attribute
- 常见的包括class、style、id等
Attribute继承
- 当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中:
禁用Attribute继承和多根节点
如果我们不希望组件的根元素继承attribute,可以在组件中设置inheritAttrs:false
- 禁用attribute继承的常见情况就是需要将attribute应用于根元素之外的其他元素
- 我们可以通过$attrs来访问所有的非props的attribute
多个根节点的attribute
多个根节点的attribute如果没有显示的绑定,那么会报警告,我们必须手动指定要绑定到哪一个属性上
子组件传递给父组件
什么情况下组件需要传递内容到父组件呢?
当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容
- 子组件有一些内容想要传递给父组件的时候
如何实现上面的操作呢?
- 首先,我们需要在子组件中定义好在某些情况下触发的事件名称
- 其次,在父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中
- 最后,在子组件发生某个事件的时候,根据事件名称触发对应的事件
Vue3与Vue2的实现稍有不同
- Vue2是在子组件的事件中通过this.emit(‘父组件v-on绑定的事件’, …args)来实现的
- v-on可以用语法糖@实现
- …args是子组件传递给父组件的值,在父组件绑定的事件中通过实参可以对应绑定
- Vue3则是多了一个emits的选项,可以是数组也可以是对象,里面是父组件绑定的事件名称,类似于props,需要先在组件中注册,其他的操作和Vue2一致
- 父组件传递事件的方式和接受变量的方式没有改变
非父子组件之间通信
在开发中,我们构建了组件树之后,除了父子组件之间的通信之外,还会有非父子组件之间的通信。
主要有两种:
- Provide/Inject
-
Provide和Inject
Provide和Inject用于非父子组件之间共享数据
比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容
- 在这种情况下,如果我们仍然将props沿着组件链逐级传递下去,就会非常的麻烦
在这种情况下,我们可以使用provide和inject:
- 无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者
- 父组件有一个provide选项来提供数据
- 子组件有一个inject选项来开始使用这些数据
实际上,可以将依赖注入看作是“long range props”,除了:
- 父组件不需要知道那些子组件使用它provide的props
- 子组件不需要知道它inject的props来自哪里
基本使用
```vue // 父组件中provide: { name: ‘zx’, age: 18 }
// 子组件中
// 孙子组件中
{{ name }} - {{ age }}
inject: [‘name’,’age’]在父组件中通过provide对象定义要传递的变量,在子组件中无需任何操作,在孙子组件中通过inject获取父组件中provide传递的变量,就可以使用了
<a name="vPscv"></a>
#### provide引入data和响应式
如果我们provide提供的变量需要从data中获取,那么可以通过this.data.xxx的形式获取,不过这时provide就不能是一个对象,而需要是一个函数,并且返回值是前面的对象
vue
provide: {
name: ‘zx’,
length: this.names.length
},
data() {
return {
names: [‘abc’,’ab’,’a’]} }
上面的写法会报错,因为this指向的是undefined,需要改成下面的形式
vue
provide() {
return {
name: ‘zx’,
length: this.names.length
}
},
data() {
return {
names: [‘abc’,’ab’,’a’]} }
还有一点,这里的length值是在一开始就获取了的,之后data中的数据发生改变(增加或减少),都无法做到响应式,所以我们需要借助于其他方法实现provide中数据的响应式<br />在vue3中,我们可以在vue中导入computed函数,通过computed包裹length的取值,就能实现响应式
vue
// 父组件中
import { computed } from ‘vue’
provide() {
return {
length: (() => this.names.length)
}
}
// 子组件中
{{ length.value }}
inject: [‘length’]
<a name="Ll6Ra"></a>
## 全局事件总线mitt库
vue2中通过新建vue实例来实现事件总线的功能。但是在vue3中,官方删除了$on、$off、$once方法,所以如果想要继续使用事件总线,要通过第三方库。
- vue3官方有推荐一些库,比如mitt和tiny-emitter
- 这里主要学习mitt的使用
<a name="WfJBX"></a>
# 插槽
在开发中,我们经常会封装一个个可复用的组件:
- 前面我们通过props传递给组件一些数据,让组件来展示
- 但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素
- 比如某些情况下我们使用组件,希望组件显示的是一个按钮,某些情况下我们希望组件显示的是一张图片
- 我们应该让使用者决定某一块区域到底该存放什么内容和元素
<a name="xi4A8"></a>
## 如何使用插槽?
- 插槽的使用就是抽取共性、预留不同
- 我们会将共同的元素、内容依然在组件内进行封装
- 同时会将不同的元素使用slot作为占位,让外部决定到底显示什么元素
具体如何使用呢?
- Vue中将<slot>元素作为承载分发内容的出口
- 在封装组件中,使用特殊的元素<slot>就可以为封装组件开启一个插槽
- 该插槽插入什么内容取决于父组件如何使用
<a name="RMIT3"></a>
## 插槽的基本使用
```vue
<my-slot-cpn>
<button>按钮</button>
</my-slot-cpn>
<h2>开始</h2>
<slot></slot>
<h2>结束</h2>
插槽的默认内容
<slot>默认内容</slot>
定义插槽时可以给插槽定义默认值,如果后续使用中没有给插槽插入值,那么插槽的位置就显示默认值。
具名插槽
使用插槽时,如果我们有多个内容需要分别传入插槽的不同位置,那么就需要用到具名插槽,因为普通插槽会把所有插入插槽的值在所有包含
// 通过给slot添加name属性来给插槽命名
<div class="header">
<div class="left"><slot name="left"></slot></div>
<div class="center"><slot name="center"></slot></div>
<div class="right"><slot name="right"></slot></div>
</div>
// 通过v-slot绑定具体的插槽
<nav-bar>
<template v-slot:left>左边</template>
<template v-slot:center>中间</template>
<template v-slot:right>右边</template>
</nav-bar>
注意:一个不带name属性的slot,会带有一个隐含的名字:default
具名插槽缩写
动态插槽名
某些情况下,我们的插槽名称可能不是写死的,一些高级的、灵活度比较高的组件就需要用到动态插槽名。
渲染作用域
在Vue中有渲染作用域的概念:
- 父级模板里的所有内容都是在父级作用域中编译的
-
作用域插槽
前面说了,父级模板只能访问父级作用域,子级模板只能访问子级作用域,但是某些情况下我们希望插槽可以访问到子组件中的内容:
当一个组件被用来渲染一个数组元素时,我们使用插槽,并且希望插槽中没有显示每项的内容
-
动态组件
动态组件是使用component组件,通过一个特殊的atribute is来实现:
<component :is="currentTab"></component>
这个currentTab的值需要是什么内容呢?
可以是通过component函数注册的组件
-
动态组件传递参数
动态组件传递参数和普通组件传递参数一样,都是父组件以属性形式传递,子组件props接收即可。
<component
name="zx"
:age="18"
:is="currentTab"
></component>
子组件中:
props: {
name: {
type: String,
},
age: {
type: Number,
},
},
keep-alive
以上面的动态组件为例,当我们将Home组件切换为About组件再切换回Home组件时,我们一开始Home组件中的值都会恢复为初始值,而不能保存修改后的值,但是某些情况下我们希望能继续保持组件的状态,而不是销毁组件,所以需要用到内置组件——keep-alive。
keep-alive属性
include - string | RegExp | Array。只有名称匹配的组件才会被缓存。
- exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。
- max - number | string。最多可缓存组件数量,一旦超过,会将最不常访问的组件销毁。
异步组件
vue-cli默认情况下,会把组件文件全部打包到一个文件中,如果项目不大,那么不会有多少影响,如果项目变大,那么会影响首屏加载速度,可以通过异步组件来进行优化,vue给我们提供了一个函数:defineAsyncComponent
defineAsyncComponent接受两种类型的参数:
- 类型一:工厂函数,该工厂函数需要返回一个Promise对象
- 类型二:接收一个对象类型,对异步函数进行配置
const AsyncCategoryVue = defineAsyncComponent(() =>
import("./AsyncCategory.vue")
);
异步组件和普通组件引入的不同在于,异步组件需要使用vue中的defineAsyncComponent来包裹,其他的使用没有区别。
还有另一种写法:
const AsyncCategoryVue = defineAsyncComponent({
loader: () => import("./AsyncCategory.vue"),
});
和上面的效果一致,没有区别
const AsyncCategoryVue = defineAsyncComponent({
loader: () => import("./AsyncCategory.vue"),
loadingComponent: loadingVue,
});
这种写法还有一个loadingComponent属性,因为是异步组件,所以难免会有加载延迟,loadingComponent的作用是指定一个组件作为占位符,等异步加载完成再替换占位符即可,开发中不常用。
组件和组件实例的关系
组件的v-model
<Input v-model="message" />
// 等同于
<Input :model-value="message" @update:model-value="message = $event" />
组件的v-model可以看作是一个属性绑定,在子组件“Input”内部需要有一个props来接收外部传递的参数
<template>
<div>
{{ modelValue }}
<button @click="btnClick">input按钮</button>
</div>
</template>
<script>
export default {
props: {
modelValue: String,
},
emits: ["update:modelValue"],
methods: {
btnClick() {
this.$emit("update:modelValue", "123");
},
},
};
</script>
<style></style>
除了props接收参数,还绑定了一个emits的事件,事件名称为“update:modelValue”,组件的v-model相当于把props传参和改变外层props传递参数这两个动作做了一个组合。
自定义组件多个v-model
组件可以绑定多个v-model属性
<Input v-model="message" v-model:title="title" />
v-model后面通过“:xxx”的形式,类似于命名
<template>
<div>
<input v-model="value" />
<input v-model="zx" />
</div>
</template>
<script>
export default {
props: {
modelValue: String,
title: String,
},
emits: ["update:modelValue", "update:title"],
computed: {
value: {
set(value) {
this.$emit("update:modelValue", value);
},
get() {
return this.modelValue;
},
},
zx: {
set(zx) {
this.$emit("update:title", zx);
},
get() {
return this.title;
},
},
},
};
</script>
<style></style>
这样在组件内部,通过computed来实现set和get,就能够实现分别显示父组件传递的v-model
Mixin
目前我们使用组件化的方式在开发整个Vue的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。
在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成:
- Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能
- 一个Mixin对象可以包含任何组件选项
- 当组件使用Mixin对象时,所有Mixin对象的选项将被混合进入该组件本身的选项中
mixin基本使用
mixin使用需要先定义一个js文件,这个文件中可以写vue相关的一些代码,比如data、methods、生命周期等。
然后在需要使用的组件中通过import导入,再通过mixins来映射到当前组件,然后就可以在当前组件使用mixin中定义的数据和函数还有生命周期等等vue的参数 ```vue // App.vueexport const demoMixin = {
data() {
return {
message: "hello demoMixin",
};
},
methods: {
foo() {
console.log("demo mixin foo");
},
},
created() {
console.log("执行了demo mixin created");
},
};
{{ message }}
我们这个App.vue中并没有定义“message”和“foo”函数,也没有定义生命周期,但是可以使用。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1644812805248-7b860c4a-91b9-4445-a0ba-e658373100fd.png#clientId=u8a1a3b0f-d397-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=340&id=u72239ea1&margin=%5Bobject%20Object%5D&name=image.png&originHeight=340&originWidth=430&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11880&status=done&style=none&taskId=u58fc0320-0985-4f60-90da-6a2d9949945&title=&width=430)<br />如果引入的mixin和本地的App.vue文件都定义了相同的变量或方法,那么本地的会代替mixin中的。
```vue
<template>
<div>{{ message }}</div>
<button @click="foo">按钮</button>
</template>
<script>
import { demoMixin } from "./mixins/demoMixin";
export default {
mixins: [demoMixin],
data() {
return {
title: "hello meto",
message: "app",
};
},
methods: {
foo() {
console.log("app foo");
},
},
};
</script>
本地定义了“foo”函数和“message”变量,那么以本地的为准
mixin的合并规则
如果mixinx对象中的选项和组件中的选项发生了冲突,vue会怎么处理?
情况一:如果是data函数的返回值对象
- 返回值对象默认情况下会进行合并
如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据
情况二:如果是生命周期
-
情况三:值为对象的选项,例如methods、components和directives,将被合并为同一个对象
比如都有methods,并且都定义了方法,那么他们都会生效
但是如果对象的key相同,那么会取组件对象的键值对,而不是mixin对象
全局混入mixin
如果组件中的某些选项,是所有的组件都需要拥有的,那么我们可以使用全局的mixin:
全局的mixin可以使用应用app的方法mixin来完成注册
- 一旦注册,那么全局混入的选项将会影响到每一个组件 ```javascript import { createApp } from “vue”; import App from “./01_mixin和extends/App.vue”;
const app = createApp(App);
app.mixin({ data() { return { ll: “love”, }; }, created() { console.log(“全体都有”); }, });
app.mount(“#app”);
在main.js文件中进行全局mixin的配置,然后所有的vue组件都会默认接收到mixin的配置
<a name="VM2Zf"></a>
# extends
extends类似于mixin:
- 允许声明扩展另外一个组件,类似于mixins
比如我们的两个组件都有“content”的变量,或者都有“bar”函数,那么就可以使用extends
```vue
// BasePage组件
<template>
<div></div>
</template>
<script>
export default {
data() {
return {
content: "Hello Home",
};
},
methods: {
bar() {
console.log("base page bar");
},
},
};
</script>
// App组件
<template>
<h2>{{ content }}</h2>
<button @click="bar">按钮</button>
</template>
<script>
import BasePageVue from "./pages/BasePage.vue";
export default {
extends: BasePageVue,
data() {
return {};
},
};
</script>
<style></style>
我们在App组件中没有定义任何变量和方法,但是页面上可以打印“content”,按钮点击也可以触发“bar”函数
认识h()函数
vue推荐绝大多数情况下使用模板来创建HTML,但是有一些特殊的场景,你需要JS的完全编程的能力,这个时候可以使用渲染函数,它比模板更接近编译器;
- 之前我们学习过VNode和VDOM的改变:
- vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM
- 其实,我们平时编写的template中的HTML最终也是使用渲染函数生成对应的VNode
- 那么,如果你想要充分的利用JS,我们可以自己来编写createVNode函数,生成对应的VNode
- 简单点说,就是在普通的vue中我们无法使用很多JS本身的语法,而只能使用经过vue包装出来的各个API,而有时某些场景下vue提供的API不能完全解决我们的问题,所以我们需要自己写JS来实现。
那么应该怎么做呢?使用h()函数:
- h()函数是一个用于创建vnode的一个函数
其实更准确的命名是createVNode()函数,但是为了简便在vue将之简化为h()函数
h()函数的使用
h()函数接收三个参数:
{ String | Object | Function }:可以是一个HTML标签名,或者是一个组件(可以是异步组件也可以是函数式组件),比如”div”
- { Object } props:与attribute、prop相对应的对象,是可选的
- { String | Array | Function }:可以是普通的字符串,例如哈哈哈中的“哈哈哈”,也可以是一个数组,数组中可以包含其他的VNode,也可以是一个对象,也是一个VNode。可以看作是一个Children。
使用render就不需要再编写template了,上面的代码中我们通过h函数创建了一个H2的标签,标签有class属性,值为title,并且有hello render的文本值。<script>
import { h } from "vue";
export default {
render() {
return h("h2", { class: "title" }, "Hello Render");
},
};
</script>
图片可以看到,完全符合h函数的内容。h()函数实现计数器案例
```javascript
虽然能够实现效果,但是编写过程非常繁琐。如果嵌套层级过深,会非常麻烦。
```javascript
<script>
import { h, ref } from "vue";
export default {
setup() {
const counter = ref(0);
const increment = () => {
counter.value++;
};
return {
counter,
increment,
};
},
render() {
return h("div", { class: "app" }, [
h("h2", null, `当前计数:${this.counter}`),
h("button", { onClick: this.increment }, "+1"),
]);
},
};
</script>
也可以使用setup的方式编写,不过这里要注意,render()函数中获取setup中暴露的变量仍然是使用this。
还可以全部写在setup内部,返回一个函数,函数的返回值是上面render函数的返回值。
<script>
import { h, ref } from "vue";
export default {
setup() {
const counter = ref(0);
const increment = () => {
counter.value++;
};
return () => {
return h("div", { class: "app" }, [
h("h2", null, `当前计数:${counter.value}`),
h("button", { onClick: increment }, "+1"),
]);
};
},
};
</script>
这样代码能更少,而且因为在setup内部,无需再通过this获取变量,但同时因为在setup内部,ref变量值的获取需要使用ref.value的形式。
自定义指令
vue本身提供了很多指令:v-show、v-for、v-model等等,除此以外,vue允许我们自定义指令。
自定义指令分为两种:
- 自定义局部指令:组件中通过directives选项,只能在当前组件中使用
- 自定义全局指令:app的directives方法,可以在任意组件中使用
我们采用三种方法来实现元素挂载后获取焦点的案例:
实现一:默认的实现方式
<template>
<div>
<input type="text" ref="input" />
</div>
</template>
<script>
import { ref } from "@vue/reactivity";
import { onMounted } from "@vue/runtime-core";
export default {
setup() {
const input = ref(null);
onMounted(() => {
input.value.focus();
});
return {
input,
};
},
};
</script>
实现二:自定义一个v-focus的局部指令 ```vue
- 实现三:自定义一个v-focus的全局指令
```vue
<template>
<div>
<input type="text" v-focus />
</div>
</template>
import { createApp } from "vue";
import App from "./10_自定义指令/03_全局实现.vue";
const app = createApp(App);
app.directive("focus", {
mounted(el) {
el.focus();
},
});
app.mount("#app");
认识Teleport
在组件化开发中,我们封装一个组件A,在另外一个组件B中使用:
- 那么组件A中template的元素,会被挂载到组件B中template的某个位置
- 最终我们的应用程序会形成一颗DOM树结构
但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到vue app之外的其他位置:
- 比如移动到body元素上,或者我们有其他的div#app之外的元素上
- 这个时候就可以通过teleport来完成
- 就是loading组件的实现
Teleport是什么呢?
- 它是一个vue提供的内置组件,类似于react的Portals
- teleport翻译过来是心灵传输、远距离运输的意思
- 它有两个属性:
- to:指定将其中的内容移动到的目标元素,可以使用选择器
- disabled:是否禁用teleport的功能
- 它有两个属性:
首先,我们在index.html新建一个div标签,id为zx
我们知道,vue最终的实现都会挂载到id为app的div节点上,而teleport组件就是为了让我们拜托这个vue的桎梏,让我们能够挂载到id为zx的div上
只要给它指定to=”#zx”,就能指定teleport内标签绑定到指定位置
通过chrome查看dom结构能看出已经成功绑定,而原本的位置会有注释节点,标识teleport的开始和结束。
认识Vue插件
通常我们向vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
- 对象类型:一个对象,但是必须包含一个install的函数,该函数会在安装插件时执行
- 函数类型:一个function,这个函数会在安装插件时自动执行
插件可以完成的功能没有限制,比如下面的几种都是可以的:
- 添加全局方法或者property,通过把它们添加到config.globalProperties上实现
- 添加全局资源:指令/过滤器/过渡等
- 通过全局mixin来添加一些组件选项
- 一个库,提供自己的API,同时提供上面提到的一个或多个功能
简单来说,插件的作用就是vue通过app.use()函数将vue的实例对外暴露,然后我们可以在暴露出来的组件实例上添加我们需要的东西,比如
export default {
install(app) {
app.config.globalProperties.$name = "zx";
},
};
上面的代码就是我们给暴露出来的组件实例添加了$name的全局变量,之所以是全局的是因为它是添加给app,也就是vue实例本身的,然后我们就可以在程序的任意地方访问到这个添加的$name变量。
在setup中,无法访问this,所以vue给我们提供了专门的方法用于访问
这个方法就是getCurrentInstance,名字就写明了作用,用来访问当前的组件实例。