这篇文章主要收录了组件中data、props、emits、provide和inject、事件总线、$refs、插槽、动态组件、异步组件、组件中使用v-model、render函数、插件、nextTick等等的使用
1 组件中的data
为什么组件中的data是函数?
- 当data是函数时,每次使用组件都会返回一个新的data对象,使用独立的地址空间
- 如果不是函数,那复用组件时 将共用数据源,不符合组件化思想
2 props
用于父组件向子组件传数据
本质就是给子组件添加自定义属性
2.1 对象形式
父组件
<son :msg="msg"></son>
data() {
return {
msg: ['a', 'b', 'c']
}
}
子组件son.vue
<span v-for="item in msg" :key="item">{{ item }}</span>
props: {
msg: {
type: Array,
default() {
return []
}
}
}
这样,子组件就可以使用父组件传过来的数据啦
3 emits(vue3)
vue3新增,用来定义一个组件可以向其父组件发射的事件。用法与props类似
数组 | 对象
2.1 vue2的做法
- 在子组件中,通过 $emit() 来发射事件
- 在父组件中,通过v-on来监听子组件事件
子组件son.vue
<buttom @click="increment">+1</buttom>
emits: ['add'],
methods: {
increment() {
this.$emit('add')
}
}
父组件
<h2>
{{ counter }}
</h2>
<son @add="addOne"></son>
data() {
return {
counter: 0
}
},
methods: {
addOne() {
this.counter ++
}
}
2.2 vue3的做法
数组形式
子组件son.vue
<buttom @click="increment">+1</buttom>
emits: ['add'],
methods: {
increment() {
this.$emit('add')
}
}
父组件
<h2>
{{ counter }}
</h2>
<son @add="addOne"></son>
data() {
return {
counter: 0
}
},
methods: {
addOne() {
this.counter ++
}
}
对象形式
对象写法的目的是进行参数验证
子组件son.vue
<buttom @click="increment">+1</buttom>
emits: {
add: null
},
methods: {
increment() {
this.$emit('add')
}
}
父组件
<h2>
{{ counter }}
</h2>
<son @add="addOne"></son>
data() {
return {
counter: 0
}
},
methods: {
addOne() {
this.counter ++
}
}
null表示无参
当有参数时
emits: {
add: (num1, num2) => {
return true
}
}
对参数有限制时(比如大于第一个参数得大于10)
emits: {
add: (num1, num2) => {
if(num1 > 10) {
return true
}
return false
}
}
虽然还是能传过去,但是会有警告;这样会清楚地知道传递的参数是有问题的
提示
官网强烈建议使用 emits 记录每个组件所触发的所有事件。
这尤为重要,因为移除了 .native 修饰符。任何未在 emits 中声明的事件监听器都会被算入组件的 $attrs 中,并将默认绑定到组件的根节点上。
4 provide和inject
用于非父子组件之间共享数据
如果通过props逐级往下传,将会非常麻烦。
无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者
父组件有一个provide选项来提供数据
子组件有一个inject选项来使用这些数据
这个和props有什么区别呢?
- 父组件不需要知道哪些子组件使用了provide的property
- 子组件不需要知道inject的property来自哪里
4.1 基本用法
父组件
provide: {
name: 'zsf',
age: 18
}
子孙组件
<h3>
{{ name }} {{ age }}
</h3>
inject: ['name', 'age']
4.2 使用data里面的数据
要想provide使用data里面的数据,并且通过this拿到
data() {
return {
names: ['zsf','aaa']
}
},
provide() {
return {
length: this.name.length,
}
}
如果不写成函数,那this指向的就不是组件实例
4.3 处理响应式
如果改变names的长度,你会发现provide里面的length没有更新。
那要想它能做到更新,需要用到vue的computed()
import { computed } from 'vue'
provide() {
return {
length: computed(() => this.name.length),
}
}
5 事件总线
5.1 vue2的事件总线
初始化
第一种方式
将一个空的vue对象挂载到Vue原型上,这样每个组件对象都可以使用~
Vue.prototype.$EventBus = new Vue()
第二种方式
创建一个模块Bus.js,导出一个空的vue对象,需要就导入
// Bus.js
import Vue from 'vue'
export const EventBus = new Vue();
实质上,它是一个不具备 DOM 的组件,它具有的仅仅只是组件的实例方法而已,因此它非常的轻便。
发送和接收事件
EventBus.$emit('emit事件名',数据)发送EventBus.$on("emit事件名", callback(payload1,…))接收
举例导入Bus.js模块的方式通过事件总线传递信息
A.vue
<p>{{msgB}}</p>
<button @click="sendMsgA()">-</button>
import { EventBus } from "../Bus.js"
data(){
return {
msg: ''
}
},
mounted() {
EventBus.$on("bMsg", (msg) => {
// a组件接受 b发送来的消息
this.msg = msg;
});
},
methods: {
sendMsgA() {
EventBus.$emit("aMsg", '来自A页面的消息'); // a 发送数据
}
}
B.vue
<p>{{msgA}}</p>
<button @click="sendMsgB()">-</button>
import { EventBus } from "../event-bus.js"
data(){
return {
msg: ''
}
},
mounted() {
EventBus.$on("aMsg", (msg) => {
// b组件接受 a发送来的消息
this.msg = msg;
});
},
methods: {
sendMsgB() {
EventBus.$emit("bMsg", '来自b页面的消息'); // b发送数据
}
}
如果只想接收一次,可以使用EventBus.$once('事件名', callback(payload1,…)
优缺点
优点
- 解决了多层组件之间繁琐的事件传播。
- 使用原理十分简单,代码量少。
缺点
- vue是单页面应用,如果在某一个页面刷新了之后,与之相关的EventBus会被移除,这样可能出现一下意外bug
- 如果有反复操作的页面,EventBus在监听的时候就会触发很多次,也是一个非常大的隐患。通常会用到,在vue页面销毁时,同时移除EventBus事件监听。
- 由于是都使用一个Vue实例,所以容易出现重复触发的情景,两个页面都定义了同一个事件名,并且没有用$off销毁(常出现在路由切换时)。
5.2 vue3的事件总线
vue3从实例中移除了off、$once方法,如果想使用全局事件总线,要通过第三方库
官方推荐mitt或tiny-emitter
使用mitt(vue3)
安装
npm instal mitt
封装一个工具eventBus.js
import mitt from 'mitt'
const emitter = mitt()
export default emitter
发送组件sent.vue
<buttom @click="btnClick"></buttom>
import emitter from './eventBus.js'
methods: {
btnClick() {
emitter.emit('zsf',参数)
}
}
接收组件accept.vue
import emitter from './eventBus.js'
created() {
emitter.on('zsf', (参数) => {
拿到参数
})
}
写法与Vue2类似,不过是使用了第三方库
多个事件的发射与监听
发送组件sent.vue
import emitter from './eventBus.js'
methods: {
btnClick() {
emitter.emit('zsf',参数)
emitter.emit('aaa',参数)
}
}
接收组件accept.vue
import emitter from './eventBus.js'
created() {
emitter.on('zsf', (参数) => {
拿到参数
})
emitter.on('aaa', (参数) => {
拿到参数
})
}
6 $refs
这种方式只需要在需要访问的子组件或元素上加个ref="xxx" 的属性
可以通过this.$refs访问到子组件或元素的信息
6.1 ref在元素上
父组件
<h2 ref='h'>
</h2>
这样,父组件就可以通过this.$refs.h获取到h2的元素对象
6.2 ref在组件上
父组件
<son ref="item"></son>
这样,父组件就可以通过this.$refs.item获取到组件son的实例对象啦,子组件的信息都可以拿到(data、methods等等)
7 插槽
7.1 动态插槽名
后面补充~
7.2 作用域插槽
先来看看什么是渲染作用域
- 父级模板里的所有内容都是在父级作用域中编译的
- 子级模板里的所有内容都是在子级作用域中编译的
比如有个父组件包了一个子组件,子组件有个title数据,想直接在父组件里面显示title,这是不可以的。
这就是渲染作用域
但是,有时候我们希望插槽可以访问到子组件中的内容
常见应用:
当一个组件用来渲染一个数组元素时,又想使用插槽,并且希望插槽中没有显示每项内容
父组件
<son :names="names"></son>
data() {
return {
names: ['zsf', 'abc', 'sss']
}
}
展示组件son.vue
<template v-for="item in names" :key="item">
<span>{{ item }}</span>
</template>
props: {
names: {
type: Array,
default: () => []
}
}
一般情况是这样的。但是,要是父元素不想使用span展示,想用其它元素展示(换句话说,父元素使用son组件时,可以决定使用什么元素展示)
展示组件son.vue
<template v-for="item in names" :key="item">
<slot>{{ item }}</slot>
</template>
父组件这样写对吗?
<son :names="names">
<button>{{ item }}</button>
</son>
不对。由于存在渲染作用域,button访问不到slot内部的item
这时你可能会问:为什么不直接在父组件遍历并展示?
上面有说到:我们希望通过复用其它组件展示,又想使用插槽。。。
这就用到作用域插槽了
用法
展示组件son.vue,在定义插槽时声明
<template v-for="(item, index) in names" :key="item">
<slot :item="item" :index="index">{{ item }}</slot>
</template>
父组件这样写
<son :names="names">
<template v-slot="slotPros">
<button>{{ slotPros.item }}</button>
</template>
</son>
这样,slotPros可以拿到slot定义的那些属性(item、index)
8 动态组件
8.1 基本用法
使用component这个内置组件的is属性
is的值可以是局部注册过的组件,或者全局注册过的。
标签栏切换案例
<div id="dynamic-component-demo">
<button
v-for="tab in tabs"
:key="tab"
:class="{ active: currentTab === tab }"
@click="currentTab = tab"
>
{{ tab }}
</button>
<component :is="currentTab"></component>
</div>
components: {
Home,
Posts,
Archive
},
data() {
return {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
}
8.2 给动态子组件传值
直接在component组件加上属性即可,就是把要传的值当component的属性
<component :is="currentTab" name="zsf" :age="18"></component>
这样,切换到的组件都会拿到name和age,通过props拿到;
当然,动态子组件也可以通过emits给父组件传事件
8.3 状态缓存
你有没有想过这样一个问题:切换子组件时,要想再切回去,以前的状态会保留吗?
不会。一旦切换,上一个子组件就会被销毁,状态没了;切换回去时,是重新创建。
每一次的切换来切换去都是销毁-重建的过程,这是耗性能的一件事
能不能将组件的状态缓存起来呢?
可以。使用内置组件keep-alive包裹起来
<keep-alive>
<component :is="currentTab" name="zsf" :age="18"></component>
</keep-alive>
keep-alive的三个属性
include
string | RegExp | Array
只有名称匹配的组件才会被缓存状态
exclude
string | RegExp | Array
匹配名称的组件不会缓存状态
max
number | string
最多可以缓存组件数量,一旦到达这数字,缓存组件最近没有被访问的实例会被销毁
提示
由于include和exclude都是根据名称匹配,所以要给对应组件加上name选项
9 异步组件
某些组件在一开始用不上,打包他们时,可以进行分包,优化首屏渲染时间
9.1 defineAsyncComponent(vue3)
Vue3提供了一个api:defineAsyncComponent
接收两种类型参数:
- 工厂函数,该工厂函数需要返回一个promise对象
- 对象,可以对异步组件进行更多配置
利用webpack的特性在Vue中使用异步组件
接收工厂函数写法
import { defineAsyncComponent } from 'vue'
const AsyncDetail = defineAsyncComponent(() => import('./AsyncDetail.vue'))
import()返回的就是promise,并且会在打包时进行分包操作
接收对象的写法
import { defineAsyncComponent } from 'vue'
const AsyncDetail = defineAsyncComponent({
loader: () => import('./AsyncDetail.vue'),
...
})
更多配置可以查看官网
9.2 和suspense一起使用
Suspense是一个内置的全局组件,该组件有两个插槽
- default 如果default可以显示,就显示default插槽的内容
- fallback 如果default无法显示,就显示fallback插槽的内容
<suspense>
<template #default>
<async-home></async-home>
</template>
<template #fallback>
<loading></loading>
</template>
</suspense>
10 组件使用v-model
能不能封装一个组件,使用的时候可以使用v-model实现双向数据绑定呢?
10.1 input使用
input元素可以直接使用v-model
<input v-model="message">
<h2>
{{ message }}
</h2>
data() {
return {
message: 'hhh'
}
}
v-model的本质是
<input :value="message" @input="message = $event.taget.value">
v-bind绑定input的value属性,然后监听input的input事件,当input事件触发时,就将输入框中的value赋值给message。这样,就是实现了双向数据绑定。
10.2 自定义组件上使用
那我要是想在自定义组件上使用呢?
父组件
<Sf v-model="message"></Sf>
data() {
return {
message: 'hhh'
}
}
<Sf v-model="message"></Sf>也可以写成
<Sf :modelValue="message" @update:model-value="message = $event"></Sf>
由于是自定义组件,通过 **event.target.value)
Sf.vue
<input v-model="value">
<h2>
{{ modelValue }}
</h2>
props: {
modelValue: String
},
emits: ['update:model-value'],
computed() {
value: {
get() {
return this.modelValue
},
set(value) {
this.emit('update:modelValue', value)
}
}
}
10.3 自定义v-model绑定多个
父组件
<Sf v-model="message" v-model:title='title'></Sf>
<h3>
{{ message }}-{{ title }}
</h3>
Sf.vue
<input v-model="value">
<input v-model="zsf">
props: {
modelValue: String,
title: String
},
emits: ['update:model-value', 'update:title'],
computed() {
value: {
get() {
return this.modelValue
},
set(value) {
this.emit('update:modelValue', value)
}
},
zsf: {
set(zsf) {
this.emit('update:title', zsf)
},
get() {
return this.title
}
}
}
11 render函数
vue推荐在绝大多数情况下使用模板来创建html,只有一些特殊的场景,才需要js的完全编程能力。
这时,可以使用render函数,它比模板更接近编译器
11.1 VNode
vue在生成真实的DOM之前,会将节点转成VNode,而VNode组合在一起形成一颗树结构,也就是虚拟DOM(VDOM)
11.2 template变真实DOM
template的里的html是怎么变成真实DOM的呢?
看这么一段代码
<template>
<div>哈哈哈</div>
</template>
经过compiler,将template转化成render函数
然后执行render函数,生成VNode
const vnode = {
tag: 'div',
children: '哈哈哈'
}
VNode最终变成真实DOM
<div>哈哈哈</div>
然后浏览器经过渲染真实DOM,显示哈哈哈
11.3 h函数
如果想充分利用js的编程能力,可以自己来编写createdVNode函数,生成对应的VNode
怎么做呢?
使用h函数
- h函数用于创建VNode
- 更准确应该叫createdVNode() ,vue将它简化为h()
参数
参数1 html标签 | 组件, 'div'
参数2 属性, {}
参数3 子节点(内容), 'hello'
使用render() 创建VNode就不需要template啦
import { h } from 'vue'
render() {
return h('h2', {class: 'title'}, 'hello world')
}
(vue2把h()当参数传给render()函数)
11.4 实现计数器
怎么使用render函数实现计数器呢?
import { ref, h } from 'vue'
setup() {
const counter = ref(0)
return {
counter
}
},
render() {
return h('div', {class: 'app'}, [
h('h2', null, `当前计数:${this.counter}`),
h('button', {
onClick: () => this.counter++
}, '+1')
h('button', {
onClick: () => this.counter--
}, '-1')
])
}
为什么render()里面可以使用this.counter获取counter?
render()内部是有绑定this的,且this指向当前组件实例
setup还可以替换掉render()这个选项
所以还可以这样写
import { ref, h } from 'vue'
setup() {
const counter = ref(0)
return () => {
return h('div', {class: 'app'}, [
h('h2', null, `当前计数:${counter.value}`),
h('button', {
onClick: () => counter.value++
}, '+1')
h('button', {
onClick: () => counter.value--
}, '-1')
])
}
}
在setup内部,所以就可以省掉this啦
注意了,在setup里面是不会自动解包的,所以要使用counter.value才能拿到counter
11.5 jsx
如果希望在项目中使用jsx,那么需要添加对jsx的支持
通常使用Babel来进行转换
来看个案例
<h2 class="title">hello</h2>
使用render() 是这样写的
import { h } from 'vue'
render() {
return h('h2', {class: 'title'}, 'hello world')
}
使用jsx之后,render()是这样写的
import { h } from 'vue'
render() {
return <h2 class="title">hello</h2>
}
如果你的脚手架不支持jsx,安装相关插件并在Babel.config.js填写相关配置即可
npm install @vue/babel-plugin-jsx -D
Babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
'@vue/babel-plugin-jsx'
]
}
12 vue插件
通常向vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
- 对象,必须包含一个install函数,该函数会在安装插件时执行
- 函数,在安装插件时自动执行
12.1 插件的强大
完成的功能没有限制:
- 添加全局方法或属性,通过把方法或属性添加到config.globalProperties上实现;
- 添加全局资源:指令/过滤器/过渡等
- 通过全局mixin来添加一些组件选项
- 一个库,提供自己的API
12.2 对象形式
export default {
install(app) {
app.config.globalProperties.$name = 'zsf'
}
}
为了防止冲突,一般给添加的属性命名加上$
怎么使用呢?
main.js
import myPlugin from './plugins'
import { createApp } from 'vue'
const app = createApp(根组件)
app.use(myPlugin)
怎么获取到刚刚添加的name呢?
vue2是这样获取的
mounted() {
console.log(this.$name)
}
换成其它生命周期或选项也是通过this.$name获取
vue3怎么获取呢?
由于setup中this没有指向当前组件实例,获取到刚刚添加的name有点麻烦,要借助一个vue得API
import { getCurrentInstance } from 'vue'
setup() {
const instance = getCurrentInstance()
console.log(instance.appContext.config.globalProperties.$name)
}
那么长一段才能获取~
12.3 函数形式
export default function(app){
app.config.globalProperties.$name = 'zsf'
}
main.js
import myPlugin from './plugins'
import { createApp } from 'vue'
const app = createApp(根组件)
app.use(myPlugin)
你会发现这种形式和对象形式差别不大,无非就是通过传入app,然后进行一系列操作~
13 nextTick
13.1 基本使用
有这么一个需求:
点击一个按钮,会修改在h2中显示的message;
message修改后,获取h2的高度;
实现有三种方式:
- 方式一,在点击按钮后立即获取h2的高度;
- 方式二,在updated生命周期中获取h2高度;
- 方式三,使用nextTick();
方式一是错误的。此时DOM并没有更新,这样获取的h2高度是不对的;
方式二确实能获取准确的h2高度,但其它节点更新,也会获取h2高度,这样做不妥;
方式三就可以,等下一次DOM更新完再获取h2高度。
13.2 原理
nextTick是如何做到的呢?
Vue 在更新 DOM 时是异步执行的;
每个数据,都会有对应的watch,当数据更新,就会执行watch()中的回调函数;
比如某个数据连续更新了100次(同步代码),界面是不会刷新100次的;
原因是Vue内部将watch() 中的回调函数放入微任务队列中;
等主线程的同步代码都执行完,再去执行watch() 中的回调函数;
这样的调度大大提高了性能;
nextTick() 内部是使用Promise 的,把DOM更新完要做的操作放到微任务队列队尾;
等DOM更新完(主线程同步代码),在执行nextTick()中的回调函数;
这样就能确保获取最新DOM的信息是准确的;
通俗一点来讲就是:
DOM的更新,获取DOM的信息,这两个操作需要排队,更新在前,获取在后。