面试的时候,也算是常考的一道题目了,而且,在日常的开发中,对于组件的封装,尤其是在
ui组件库
中,会用到很多,下面,就来详细的了解下,通过这篇文章的学习,可以提升项目中组件封装的灵活性,可维护性,话不多说,先从大致的通信方式分类说起,然后依次非常详细地介绍,看完整篇文章,你能大致熟悉vue中的各种组件通信机制,然后就能够封装更加高阶的组件啦!
通信目录
-
props
-
$emit
-
$attrs
&$listeners
-
provide
&inject
-
vuex
-
eventBus
-
$refs
-
slot-scope
&v-slot
-
scopedSlots
-
$parent
&$children
&$root
1.props
-
基本使用
props是父组件传子组件的传参方式,可以看到父组件中传入了一个parentCount
变量,通过prop传递给了子组件,子组件拿到的count
就是通过prop传递过来的值,所以子组件中显示的1
// Parent.vue
//Child.vue
{{count}}
-
类型限定
可以看到上面的代码中,给props
中的count
变量设定了Number的类型限定,如果改为String
类型,可以看到如下的报错
可以设置的类型有如下几种类型
-
String
-
Number
-
Boolean
-
Array
-
Object
-
Date
-
Symbol
-
Function
-
Promise
前面的基本基本类型就不详细讲解了,主要对于Function写一个实际的案例,还是按照上面的案例,做一个点击数字+1的方法吧,也是通过父传子的props
方式
//Parent.vue
//Child.vue
{{count}}
-
类型限定-进阶
不仅能传变量,还能传递方法,能对于变量的数据类型加以控制,当然,props远比你想象的更强大,他不仅可以传入一个字符串,数组,还能传入一个对象,在对象中可以进行设置必填,设置默认值,自定义校验等操作
export default {
name: "Child",
props:{
//你可以让count不仅可以传数值型,还能传入字符串
count:[Number,String],
//设置必填
count:{
type:Number,
require:true
},
//设置默认值
count:{
type:Number,
default:100
},
//设置带有默认值的对象,对象或数组默认值必须从一个工厂函数获取
user:{
type:Object,
default: ()=>{return {name:'jj'}}
},
// 自定义校验,这个值必须匹配下列字符串中的一个
user:{
type:String,
validate: (value)=>{return ['bob','jack','mary'].findIndex(value)!==-1}
}
}
}
-
单向数据流
prop还有一个特点,是单向数据流,你可以理解为自来水从上流至下,父组件中值的变化能传递到子组件中更新,反之,子组件中的更新是不会触发父组件的更新的,这样可以防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
//Parent.vue
父级别{{parentCount}}
//Child.vue
{{count}}
上面的子组件中,点击按钮,改变count
,子组件的数值会进行累加,但是会发生报错,同时,父组件的value值并不会发生更新,这就是因为props是单向数据流的,至于如何实现双向数据流,让子组件的更新能同时更新父组件,在接下来的讲解中,我会揭开它的面纱
2. $emit
-
基本使用
-
$emit
是子传父的通信方式,官方说法是触发当前实例上的事件。附加参数都会传给监听器回调,$emit接收两个参数,第一个是事件名,你可以理解为click,change
等事件监听的事件名,然后呢,这个事件名是绑定在父组件当中的,第二个参数就是传递的参数啦,话不多说,上个栗子瞅瞅
//Parent.vue
//Child.vue
子组件点击提交的时候,相当于触发了父组件的自定义事件,父组件能够在自定义事件中获取到子组件通过$emit
传递的值,所以,当子组件的按钮点击的时候,控制台会打印出test
-
父子组件双向绑定
-
1.
props
结合$emit
方式实现双向绑定
现在,让我们再次回到之前描述的单向数据流的场景中,由于有的时候可能因为项目需要,确实需要实现双向的数据流绑定,可以看到依旧是单向数据流的方式,父组件的parentCount
通过子组件的props
进行值的传递,子组件点击按钮的时候,触发父组件的自定义事件,改变父组件的值(进行累加操作),然后父组件通过props
,将更新传递到子组件,实现了父子组件的双向数据绑定。
//Parent.vue
父级别{{parentCount}}
//Child.vue
{{count}}
-
2.使用
.sync
修饰符
你可以觉得上面的写法有点长,那么,你可以使用一个vue 的语法糖(指在不影响功能的情况下,添加某种方法实现同样的效果,从而方便程序开发,例如v-bind
和v-on
,可以改写为:
以及@
),那么,我们下面就用.sync
修饰符来重写父组件吧,子组件的写法依旧不变,实际运行发现效果是一样的
父级别{{parentCount}}
3.$attr
& $listener
-
基本使用
首先引用官网的概念,
$attrs
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)
。当一个组件没有声明任何prop
时,这里会包含所有父作用域的绑定
(class 和 style 除外),并且可以通过v-bind="$attrs"
传入内部组件——在创建高级别的组件时非常有用。
$listeners
包含了父作用域中的(不含 .native 修饰器的) v-on 事件监听器
。它可以通过v-on="$listeners"
传入内部组件——在创建更高层次的组件时非常有用。
让我们来看个例子
//Parent.vue
父级别{{parentCount}}
//Child.vue
让我们来康康控制台的输出,可以看到的是,$attrs
主要是收集父组件的属性,毕竟正如其名嘛,如果我猜的不错全写就是attributes,而$listeners
就是相当于eventListeners
,那就是收集父组件的事件呗,两兄弟各负其职,分工明确。虽然自己的工作职责明确了,但是管辖范围也可以从中看出,只有没有在子组件中的props
注册的父组件属性,才能被$attrs
监听到,而没有.native
修饰符的事件,才能被$listeners
所监听到。
与此同时,调用this.$attrs.name
可以拿到值为cooldream,并且,调用this.$listeners.click()
可以触发父组件中绑定click事件的方法
,即案例中的handleOne
-
inheritAttrs
还是上面的案例,我们来看一下html结构,可以看到我们的name
由于没有在props
中进行注册,编译之后的代码会把这些个属性都当成原始属性对待,添加到 html 原生标签上
我们来给子组件修改一下,添加了一个inheritAttrs:false
,可以看到name
属性被隐藏了
4.provide
& inject
-
基本使用
首先引用官方的一段话就是
provide
和inject
主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。值得一提的是,provide
和inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
说白了就是在父组件中注册了provide
之后,在所有的子组件,子子组件,子子孙孙,都可以注册inject
拿到父组件中的provide
中的东西,简直是一笔财富,子子孙孙无穷尽,所有子子孙孙都能拥有的遗产啊,下面就来看看如何注册自己的财产,以及子子孙孙怎么拿到这笔遗产吧,为了证实我们的设想,这里我们新增一个子组件,现在的结构是Parent.vue=>Child.vue=>ChildPlus.vue
,可以看到,无论是在Child.vue
还是在ChildPlus.vue
中,只要inject中注册了name
,都可以访问到provide中的name
值,也就是说只要是子组件,都可以访问,无论层级都多深。
//Parent.vue
//Child.vue
//ChildPlus.vue
-
使用
provide & inject
实现不白屏的刷新
上面的案例中,只是实现了provide
继承变量,其实,他还可以继承方法,用一个变量控制
的渲染,将他设为false,就会页面重新渲染,然后在nextTick
的回调完成后,再将页面进行显示,相比于原生的js刷新页面,他不会白屏闪一下,用户体验质量会高很多。
//App.vue
//Parent.vue
-
v-if和v-show的区别
这里捎带提一下上面为什么是要使用v-if
,而不是v-show
,首先来看看两者的区别
v-if
在切换过程中条件块内的事件监听器和子组件都会被适当地销毁和重建,但它也是惰性
的,如果在初始渲染条件为假的时候,则说明都不做,只有当条件第一次为真的时候,才会开始渲染,它的本质其实就是display:none;
v-show
不管初始条件是真是假,都会被渲染,如果切换频繁,那么用v-show
,如果很少改变,就用v-if
,它的本质其实就是visibility:hidden;
所以,上面就需要使用v-if
,因为刷新页面的主要目的就是要重新渲染页面,而v-show
并不会重新渲染,只是类似捉迷藏似的,很简单的控制显示和隐藏
-
nextTick()
那么,上面用到的nextTick()究竟又是干啥的呢,先来看看官方的解释
Vue
异步执行DOM 更新
。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个watcher
被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作
上非常重要。然后,在下一个的事件循环“tick”
中,Vue 刷新队列并执行实际 (已去重的) 工作。
其实说白了就是你可以理解成DOM更新完成之后的一个回调,只有当DOM更新完成之后,再将isRouterAlive
设置为true,将
中的内容再次显示出来,更多关于nextTick()
的相关内容这边也不再做拓展啦,想看的可以点击这里,https://www.jianshu.com/p/a7550c0e164f进行阅读
Vuex
-
基本概念
vuex
可以理解为整个Vue程序
中的全局变量,但他和以前概念中的全局变量又有所不同,他也是响应式的,而且,你不能直接修改vuex中的变量,只能通过显式地commit=>mutation
来进行数据的改变。
-
五大模块
-
State => state里面存放的是变量,如果你要注册全局变量,写这里
-
Getter => getters相当于是state的计算属性,如果你需要将变量的值进行计算,然后输出,写这里
-
Mutation => 修改store中的变量的方法,如果你要改变变量的值,就写这里
-
Action => actions提交的是mutations,相当于就是改变变量的方法的重写,但是,actions是可以进行异步操作的
-
Module => 将整个Vuex模块化,主要的特点就是namespaced,所有的访问都要经过命名空间
-
基本定义
首先在src
文件目录下新建store
文件夹,在store
文件夹中新建module
文件夹以及index.js
,然后在module
中新建自己的功能模块的js文件,例如我这边新建了一个user.js
,专门存放用户相关的全局变量,所以目录如下
├─ src //主文件
| ├─ store //vuex全局变量文件夹
| | |- index.js //store主文件
| | └─ module //store模块文件夹
| | | └─ user.js //存放user相关的全局变量
| ├─ main.js
接下来,在相应的文件夹中写入以下内容
//index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
user
}
})
//user.js
const state = ({ //state里面存放的是变量,如果你要注册全局变量,写这里
username:'',
});
const getters = { //getters相当于是state的计算属性,如果你需要将变量的值进行计算,然后输出,写这里
fullName(state){
return state.username + '大王';
}}
;
const mutations = { //修改store中的变量的方法,如果你要改变变量的值,就写这里
SET_username(state, value) {
state.username = value;
},
};
const actions = { //actions提交的是mutations,相当于就是改变变量的方法的重写,但是,actions是可以进行异步操作的
setUsername(content) {
content.commit('SET_username');
}
};
export default{
namespaced:true,
state,
getters,
mutations,
actions
};
//main.js
import Vue from 'vue'
import App from './App'
import store from './store/index'
new Vue({
el: '#app',
store,
components: { App },
template: ' '
});
在上面的代码中,已经实现了vuex的模块化,定义State,Getter,Mutation,Action
等所有基本的功能。
页面中的使用
-
直接使用
state => this.$store.state.user.username
getter => this.$store.getters.user.fullName
mutation => this.$store.commit('user/SET_username','cooldream')
此时的username
值为cooldreammutation => this.$store.dispatch('user/SET_username')
action未找到传值的方法
-
使用辅助函数
上面大致罗列了vuex的所有基本使用方法,使用全局变量,同样可以实现vue组件之间的通信,还是挺方便的
EventBus
-
基本概念
EventBus
又称为事件总线,它就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件,你可以理解为现实生活中的中介,一般项目小的时候可以推荐使用EventBus
,但是,中大型项目还是更加推荐使用Vuex
。
-
基本使用
它主要分为两种使用方式,局部和全局
局部:你可以通过新建一个eventBus.js,它根本不需要任何DOM,只需要它创建的实例方法即可。
import Vue from 'vue'
export const EventBus = new Vue()
全局:在
main.js
中将EventBus挂载到Vue实例中即可实现全局
Vue.prototype.$EventBus = new Vue()
我们先来聊聊第一种局部使用的方式,注意,这里用父子组件只是为了方便查看效果,两个毫无关系的组件也可以使用EventBus
的方式进行传值,值得注意的是,我们是父组件mounted生命周期
,而子组件created生命周期
中执行,如果反转,将无效果,所以,一定要先开启监听器,再发送请求,先后顺序应该是$on=>$emit
//Parent.vue
//Child.vue
{{username}}
所以,可以得到的结论就是
// 发送消息
EventBus.$emit(event: string, args(payload1,…))
// 监听接收消息
EventBus.$on(event: string, callback(payload1,…))
全局使用的方式如下
//main.js
let EventBus = new Vue();
Object.defineProperties(Vue.prototype, {
$bus: {
get: function () {
return EventBus
}
}
});
//Parent.vue
//Child.vue
{{username}}
采用全局的方式同样可以达到相同的效果
-
注意点
值得注意的是,由于Vue是SPA(单页面富应用),如果你在某一个页面刷新了之后,与之相关的EventBus
会被移除,这样就导致业务走不下去。还要就是如果业务有反复操作的页面,EventBus
在监听的时候就会触发很多次,也是一个非常大的隐患。这时候我们就需要好好处理EventBus
在项目中的关系。通常会用到,在vue页面销毁时,同时移除EventBus事件监听,用过定时器出过问题的应该都深有印象,全局的东西,在页面跳转之后是依然存在的,所以确实有销毁的必要。上面的案例中可以使用Event.$off('sendMessage',{})
或者this.$bus.$off('sendMessage',{})
来清空,当我们绑定的监听器过多,想一次全部清空的时候可以使用Event.$off()
或者this.$bus.$off()
来完成。其实上面所说的就是大家耳熟能详的Pub/Sub(发布/订阅者模式)。$emit
负责发布,$on
负责订阅。
$refs
使用$refs
同样可以拿到值和方法,当然仅限于父组件取子组件中的值,但是可以深度遍历,取孙子组件的值,孙子的孙子等,我们可以在子组件中注册refs
,输出查看值
//Parent.vue
//Child.vue
可以看到子组件中的username
的值,以及子组件中定义的hiName
方法
接下来我们可以用方法访问变量的值以及调用方法
console.log(this.$refs.childrenComponent.username);
this.$refs.childrenComponent.hiName();
接下来我们看看如何取孙子组件的值
//Parent.vue
//Child.vue
//ChildPlus.vue
可以看到,我们在输出结果中的$children[0]
,看到了孙子组件中定义的变量,可以通过this.$refs.childrenComponent.$children[0].username
访问到孙子组件中的变量。这里值得提到的一点是,ref绑定的值是可以重复的
。如果一个模板中,绑定了5个相同值的ref,那么,获取这个$refs
的时候,你会拿到一个长度为5的数组列表,没试过的童鞋可以试试哦。
slot-scope & v-slot
一般用过element-ui
的table
组件的人都对这个slot-scope
比较熟悉,在表格的模板中可以拿到当前行的数据
{{scope.row.username}}
不过,slot
和slot-scope
已经要被整合了,新标准就是v-slot
,所以,接下来还是着重讲讲v-slot的方式吧,拥抱新技术
-
普通插槽
还记得上面讲过的props
吗,就是父组件传给子组件变量,方法等,相当于是父传子js代码
,那么,现在这个插槽,就相当于是父传子html代码
。正如他的名字,插槽,父组件有值就用父组件的,没有就用子组件中的默认值,插槽插上了,那就显示插上的内容,没有插上,就显示默认值。其实就是那么简单。案例还是上面的Parent.vue & Child.vue
,具体的组件引入我就不重复写了,代码如下,可以看到最后显示的内容是Header
,因为父组件传递给了子组件html内容
//Parent.vue
Header
//Child.vue
defaultContent
-
具名插槽
具名插槽和普通插槽最大的区别在于,给普通插槽赋予了名字,这就是最大的一点区别,这就有点类似ES6
中新出的特性,解构赋值
。现在我们来改写上面的案例,新增加一个明明为header
的插槽,并且,用v-slot:header
的方式给传入的html代码
进行命名,可以看到成功地将名字为header的插槽替换了,所以输出结果为Header defaultContent
//Parent.vue
Header
//Child.vue
defaultHeader
defaultContent
介绍完了具名插槽以后,值得注意的一点是,普通插槽其实也是具名插槽,它的名字为default
,我们来改写第一个案例的代码,可以发现运行效果是一样的,输出结果为Header
//Parent.vue
Header
//Child.vue
defaultContent
所以,第一个案例的效果相当于
//Parent.vue
Header
//Child.vue
defaultContent
插槽就像锁一样,传入的html代码
就像钥匙,一把钥匙,就能开所有的锁,如果有两个名为header
的插槽,一个名为header
的传入模板,那么,最终会显示两次header
模板,例如下面的代码,输出结果为Header Header default
//Parent.vue
Header
//Child.vue
defaultContent
defaultContent
default
-
作用域插槽
上面主要是讲了下插槽的基础使用,现在要讲的作用域插槽,就和组件通信有关啦,是父组件拿子组件中的信息。可以看到我们在插槽上动态绑定了data
,值为user对象
,那么,在父组件引用中的v-slot值.data
就可以访问到子组件中的user对象
的值了。所以父组件中的输出结果就是jack 18
。也就是说element-ui的table组件
用的就是:row
//Parent.vue
{{scope.data.name}}
{{scope.data.age}}
//Child.vue
default
值得注意的是,默认插槽的缩写语法不能与具名插槽混用,下面的代码将会报错,报错如下
//Parent.vue
{{scope.data.name}}
{{other.data.age}}
//Child.vue
default
所以应该改为下面的这种方式
{{scope.data.name}}
{{other.data.age}}
-
动态名称插槽
通过[ ]
将变量名称括起来,可以实现动态名称的插槽,配合条件渲染,选择相应的具名插槽,能让组件的封装更加地灵活,因为args=other
,所以,输出结果为default jack
//Parent.vue
{{scope.data.name}}
//Child.vue
default
-
具名插槽简写(语法糖)
v-slot:other => #other
,值得注意的是,v-slot => v-slot:default
,但是# !=> v-slot:default
,使用语法糖必须加上名称,即#default => v-slot:default
-
$scopedSlots
还记得上面的v-slot吗,他们有很多相同点与不同点,我们先对他们进行一下比较
相同点:
都是作用域插槽
不同点:
v-slot是模板语法,即写在中的语言
|$scopedSlots是编程式语法,即写在render() || Jsx的语言
首先来看下一个使用v-slot
写的案例
//Parent.vue
{{scope.data.name}}
//Child.vue
-
render()方式使用$scopedSlots来改写,下面的代码运行以后和上面的效果是一样的,总结而言render函数就是
h('标签名字',{标签属性},[标签内容])
//Parent.vue
{{scope.data.name}}
//Child.vue
-
Jsx方式使用$scopedSlots来改写
Jsx方式我就不再写案例演示了,具体的可以查看这里https://juejin.im/post/5c65511ce51d457fd23cf56b#heading-4
-
$parent & $children & $root
$parent 写在子组件中,用于获取父组件实例,可无限向上遍历
$children 写在父组件中,用于获取子组件实例,可无限向上遍历
$root 写在任意组件中,用于获取Vue实例,它的$children就是App.vue实例
$parent
用于子组件中拿到父组件中的实例,可以多层级地遍历当前子组件中的父组件实例,先来看下下面的例子,还是Parent.vue & Child.vue
,所以,按照层级顺序应该是App.vue => Parent.vue => Child.vue
//Parent.vue
//Child.vue
我们可以在输出内容中找到Parent.vue
中的变量和方法,然后可以在当前的实例中找到$parent
,而这个实例就是App.vue
,而$children
同理,是写在父组件中获取子组件实例的,同样可以无限向下遍历实例。这里,我们就不再写案例演示了。而$root
是用于获取根组件实例的,例如无论是写在Parent.vue中还是Child.vue中
,都可以获取到App.vue的实例
我们可以看看$root
输出的内容,App.vue
的实例可以在$children中访问,App.vue
中的变量和方法也是在这里
以上就是我所理解的vue中的所有组件通信方式,阅读完所有的内容,我相信还是很有帮助的。文中有很多可能讲解的不是很详细,具体深入的可以自行百度查看,但是,基本的案例,都是我一个个在实际项目中运行过的,希望能对大家有所帮助。
参考:Vue.js 父子组件通信的1212种方式
vue官网
Vue事件总线(EventBus)使用详细介绍
Vue.js 你需要知道的 v-slot (译)