Vue组件间11种通信方式的简要介绍
Vue组件的通信方式大致有这11(12)种
- 常用的Props
- $attrs & $listeners
- provide & inject
- $parent & $children
- $root
- 自定义事件的 $emit & $on
- sync语法糖(废弃的修饰符 转 语法糖)
- vModel语法糖
- 粗暴的$refs获取子组件
- EventBus
- Vuex
- 废弃的$boradcast & $dispatch
我只使用过前11种,最后一个因为已经废弃,也不作为语法糖,所以大家有兴趣可以单独去了解一下
1. props的使用
props是最基础的组件单项数据流通信,一般代码如下:
// 创建全局的tips组件
Vue.component('tips',{
props:['value'],
render: function (h) {
return (
<div class='tips-cover'>
<div class="tips-msg">{this.value}</div>
</div>
)
}
})
// 父组件中引入子组件
<tips v-if="show_tips" value="这是个基本的弹层"></tips>
// ...
export default {
// ...
mixins: [tipsMixin],
//...
}
// ...tipsMixin中的内容
export default {
data () {
return {
show_tips: false
}
},
methods: {
showTips () {
console.log(this)
this.show_tips = true
setTimeout(() => {
this.show_tips = false
},3000)
}
}
}
如果只使用props往往会存在一个问题,因为props是单向数据流,也就是数据只能由父到子,本身不提供子组件直接改变父组件的方式,只能父组件把自己的方法传给子组件,再在子组件中回调父组件的方法,举个简单的例子,如果我写一个名为tips的弹层提示组件,如果我把控制组件显示逻辑的变量写在了子组件里,父组件如何去改变子组件的变量值来显示或隐藏子组件?如果不借助其他的方法似乎不能吧?所以只能把控制显示的变量和相关方法都写在父组件里,每个父组件都mixin相关的data和methods。感觉这样写比较死板,比如我要维护这个组件的时候,需要改对应组件的vue/js文件,还要去修改父组件的mixin.js。
2. $attrs & $listeners
$attrs & $listeners 的初始化发生在生命周期 beforeCreate 之前的 initRender 函数中,使用 defineReactive(defineProperty) 将$attrs和$listener绑到了vm(vue对象)上,如果父组件传递的参数发生变动,会触发updateChildComponent, 并对值进行更新
vm.$attrs = parentVnode.data.attrs || emptyObject;<br>
vm.$listeners = listeners || emptyObject;
$attrs表示父组件传递下来的props的集合
$listeners表示父组件传递下来的invoker函数的集合
举个例子:
// 父组件中引用子组件
<attrAndListenersCom @setGrandData="setGrandData" :fatherdata='fa_data'></attrAndListenersCom>
在子组件中$attrs就是{fatherdata: 父组件中fa_data的值}
在子组件中$listeners就是 {setGrandData: ƒ}
然后子组件可以使用如下的方法,将父组件的参数继续传递给自己的子组件
从而实现了父组件对孙子组件之间的数据传递
// 子组件中再引用其他子组件
<attrAndListenersComCom v-bind="$attrs" v-on="$listeners"></attrAndListenersComCom>
孙子组件简易代码如下
<template>
<div>孙子引用父组件的变量:{{$attrs.fatherdata}}</div>
<div class="btn" @click='test'>点我触发一些操作</div>
</template>
<script>
methods: {
test () {
this.$emit('setGrandData', '孙子组件来了!')
}
}
</script>
点击按钮,可以改变三个组件中,对fa_data的引用,即父组件的fa_data,子组件的$attrs.fatherdata,和孙子组件中的$attrs.fatherdata
值得注意的是,$attrs中不会出现被props引用过的值,也就是如果子组件的props引用了fatherdata,那他的$attrs就是空的。这个过程发生在createComponent(组件创建)中,会调用extractPropsFromVNodeData函数,其内部的checkProp函数会删除$attrs中在props中出现的变量。
还有就是:$attrs的赋值过程发生在updateChildComponent中,是一层一层往下传递的,所以你在层级较高的组件中对$attrs进行watch,watch的回调经常会被触发多次。但这并不是因为每一层都会响应一次变动,而是有点类似ReactHook中 useMemo 记忆组件的感觉:父组件有2个子组件a和b,对a中参数的改变有可能会触发b的重新渲染。个人理解这里也是一个道理,你的各种异步操作对父组件data的操作,触发了updateChildComponent,最后都会响应到深层子组件/$attrs的Watcher上。
个人对 $attrs 使用场景的理解是:参数的逐层传递
3. provide & inject
inject的初始化发生在beforeCreate与created之间,先于provide的初始化
callHook(vm, 'beforeCreate');
initInjections(vm); // 初始化inject
initState(vm);
initProvide(vm); // 初始化provide
callHook(vm, 'created');
inject初始化相关源码:
function initInjections (vm) {
/**
initInjections的功能就是把inject挂载在vm上
**/
var result = resolveInject(vm.$options.inject, vm);
if (result) {
toggleObserving(false);
Object.keys(result).forEach(function (key) {
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], function () {
...
});
} else {
defineReactive(vm, key, result[key]);
}
});
toggleObserving(true);
}
}
/**
resolveInject的功能就是遍历所有的父组件,拿到他们的provide
**/
function resolveInject (inject, vm) {
if (inject) {
var result = Object.create(null);
var keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key === '__ob__') { continue }
var provideKey = inject[key].from;
var source = vm;
/**
这个地方也有bug,source为当前vue对象,
inject初始化发生在provide之前,
所以这里的source._provided第一次必为undefined
**/
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey];
break
}
source = source.$parent;
}
if (!source) {
if ('default' in inject[key]) {
var provideDefault = inject[key].default;
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault;
} else if (process.env.NODE_ENV !== 'production') {
warn(("Injection \"" + key + "\" not found"), vm);
}
}
}
return result
}
}
由此可以看出,inject继承自最近父组件的provide,一旦找到就会break出寻找_provided的while循环,如果没有会一直找到根节点
顺便提下个人主观的issue: 寻找_provided的while循环中,进入循环的source是不是一定没有_provided?因为当前vm的provide初始化发生在inject初始化之后,所以这时候一定是undefined...吧?
provide初始化相关源码:
function initProvide (vm) {
var provide = vm.$options.provide;
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide;
}
}
由此可以看出provide中的变量并没有做过多处理,只是将_provide作为provide绑在了vm上,组件自身使用自己的provide属性需要这样写: this._provide.xxx, _provide不是响应式的,改变它的值不会引起view的变化
其使用方式为:
// 父组件:
provide: {
fa_provide: 一个常量
}
// 或
provide () {
return {
fa_provide: this.data中的变量
}
},
// 或
provide () {
return {
// fa_provide: this.obj.a
fa_provide: this.methods中的方法
}
},
// 子组件:可以引用/覆盖/重写上层的provide
inject: ['fa_provide'],
provide: {
fa_provide: 另一个常量
}
// 孙子组件中也可以引用到父组件的provide
inject: ['fa_provide'],
然后通过this.fa_provide引用常量/变量,或者调用方法
个人对provide & inject 使用场景的理解是,跨级传递常量/变量/方法,供深层级子组件使用
4. $parent & $children
$parent & $children属性的定义是发生在initMixin中。
initMixin仅仅只做了在Vue的原型上挂了个_init。
_init函数是在Vue构建函数中唯一被调用的函数。
function Vue (options) {
this._init(options);
}
扩展阅读:
在_init函数中
Vue.prototype._init = function (options) {
...
/** 在这之前options中的结构只包含
{
parent: VueComponent,
_isComponent: boolean,
_parentVnode: VNode
}
这里的options还是最原始的options
**/
if (options && options._isComponent) {
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
...
initLifecycle(vm);
...
}
// initInternalComponent有这么几行代码
var opts = vm.$options = Object.create(vm.constructor.options);
opts.parent = options.parent;
opts._parentVnode = parentVnode;
这里会把你写的Vue文件中的data啊、methods啊,利用ES6的Object.create打到$option的__proto__上,其实你平时初始化Vue时调用的opts.data,opts.props之类的属性,并不是直接在opts上的,而是通过这里扩展在原型链上的,parent也在扩展范围内~
扩展阅读结束~回到正文
$parent & $children 的定义实际发生在initLifecycle中
function initLifecycle (vm) {
var parent = options.parent;
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
vm.$parent = parent;
vm.$root = parent ? parent.$root : vm;
vm.$children = [];
}
使用方式也很简单,$children会获取到一个包含所有子组件VueComponent对象的的数组,$parent会获取到父节点对应的Vue/VueComponent对象,你可以通过如下方式进行操作
// 此处data_name代指data属性值,function_name代指方法名
this.$children[index].children_data_name
this.$children[index].children_function_name
this.$parent.$parent.parent_data_name
this.$parent.$parent.parent_function_name
this.$root.root_data_name
this.$root.root_function_name
值得注意的是,我们通过脚手架构建出来的Vue项目,$root是在main.js里写的那个new Vue({router,.......}).$mount('#app'),而不是我们写的那个App.vue
如果在层级很深的时候想拿到App.vue内的data,可以this.$root.$children[0].app_data_name
5. $root
在上面第3节的结尾有一起提到~
PS: 后面的方法比较常用或者是语法糖,我准备划水通过了~
6. 自定义事件的 $emit & $on
$emit & $on是 Vue原型链上本来就绑定好的函数,不是专门为了组件间通信而建立的,他们还能用来触发一些钩子函数。
父组件中如下引用子组件:
<emitCom @reverse='这里写父组件的方法名'></emitCom>
...
methods: {
reverse (val) {
this.father_name = val // 这里val为子组件触发时传递的参数
}
}
子组件如下触发
this.$emit('reverse','你被子元素触发了')
7. sync语法糖
sync等于是帮你定义了一个自定义函数,名为'update:' + 你v-bind的属性名
父组件中如下引用子组件:
<syncCom :xxx.sync="father_name"></syncCom>
// 等效于
<syncCom :xxx="father_name" @update:xxx="val => {father_name = val}"></syncCom>
子组件如下触发
this.$emit('update:xxx', '改变父组件!!!')
比较贴近生活的例子: elementUI中el-dialog中对显隐变量visible的传递是使用的:visible.sync
8. vModel语法糖
万变不离其宗,这个vModel也是语法糖,效果就是平时写vModel双向绑定+$emit的感觉差不多
父组件中如下引用子组件:
<child v-model="total"></child>
// 等效于
<child :xxx="total" @input='val => {total = val}'></child>
默认状态下:子组件如下触发
this.$emit('input', xxx)
你也可以自定义传过来的变量名和方法名
model: {
prop: 'parentValue', // 默认值 value
event: 'change' // 默认值 input
},
9. 粗暴的$refs获取子组件
$refs一般被默认为想要进行一些Dom操作的时候才被使用,其实他也能够获得带有ref属性的子组件对象。
父组件中
<loading ref="loading"></loading>
<script>
showLoading () {
// 可以直接调用子组件中的方法,其实和$children相似
this.$refs.loading.showLoading()
setTimeout(() => {
this.$refs.loading.closeLoading()
},3000)
}
</script>
如果有大佬或者有兴趣的小伙伴可以考究一下$refs的性能问题,便利蜂的大佬说$refs是操作了DOM,但是如果作用于Vue子节点的时候返回的明明是VueComponent对象,我感觉和$children没太大区别,即时有区别也是因为$children是一定会初始化的,而$refs是在ast模板解析的时候根据你template中的ref来初始化的,如果你不写ref那性能必须比你写要好一丢丢~但是不管你写不写children,只要你有子组件就会有$children。可能就这些差异吧。
10. EventBus
- 引入单独的空Vue文件
- 在需要接受响应的页面,引入该Vue文件,定义$on
import Bus from '@/api/bus.js'
...
Bus.$on('getTarget', target => {
...
});
3.在需要发起通知的页面,引入该Vue文件,定义$emit
import Bus from '@/api/bus.js'
...
Bus.$emit('getTarget', 123);
11. Vuex
不适合作为小知识点扩展,大致举个例子,就是有些父子页面、兄弟页面或者更复杂关系的页面,会使用Vuex来共享数据,当一个页面改变了数据,在另一个页面我能通过compute(+watch),来做出相关的处理。嗯。。。我就当你们都懂了~
12. 废弃的$boradcast & $dispatch
这个我没有自己使用过,$dispatch 和 $broadcast在2.x版本已被废弃,有兴趣的小伙伴自行了解吧~