Vue.js 在现今使用有多普遍不用多说,而 Vue 的一大特色就是组件化。本期要讲的,即是 Vue 组件间通讯方式的总结,这也几乎是近年 Vue 面试中的必考题。注:文中示例都基于 Vue 脚手架讲解,会用到一些 Element UI 示例。javascript
【前端进阶之路】会做为一个新系列连载,后续会更多优质前端内容,感兴趣的同窗不妨关注一下。 文章最后有 交流群 和 公众号,能够一块儿学习交流,感谢🍻。html
组件是能够复用的 Vue 实例。 — Vue 官方文档;前端
在进入主题以前,仍是决定先简单聊聊组件。在 Vue 中,根据注册方式的不一样,能够分为:vue
顾名思义,全局注册的组件,能够用在 Vue 实例的任意模板中。可是带来的隐患是,在 webpack 模块化构建时,即使你没有在项目中使用这个组件,依然会打包到最终的项目代码中。而局部组件,则须要在使用到的实例中注册该组件。java
// 全局注册
// install.js
import Icon from './Icon.vue';
const install = {
install:function(Vue){
Vue.component('VIcon', Icon);
}
};
export default install;
// main.js
import install from './install.js'; // 引入全局插件
Vue.use(install); // 注册
// 局部注册
import VIcon from './Icon.vue';
export default{
components: {
VIcon
}
}
// 使用
<v-icon> </v-icon> 复制代码
根据应用场景的不一样,又能够分为:node
以上就是组件的简单介绍,那咱们到底为何要推崇组件化?组件化有什么好处?复用?我我的认为组件化最大的好处,即是解耦,易于项目管理。因此在大型项目管理中,组件化是很是有必要的。固然,这并非今天学习的重点,之后有机会再聊。webpack
正由于在 Vue 中到处都是组件,而咱们也偏向于组件化、模块化。那咱们在一堆组件中,便须要解决一个问题 — 组件间通讯。下面,咱们就进入今天的主题,Vue 的组件间通讯。git
组件间通讯是咱们在 Vue 项目中不可避免的问题,深入了解了 Vue 组件间通讯的几种方式,才能让咱们在处理各类交互问题时游刃有余。github
Vue 中,最基本的通讯方式就是 Props,它是父子组件通讯中父组件传值给子组件的一种方式。它容许以数组形式接收,可是更推荐你开启类型检查的形式。更详细的类型检查前往 vue 文档。web
// communication.vue
<communication-sub v-bind="dataProps"></communication-sub>
// v-bind="dataProps" 等同于 :title="title",适用于多个参数一块儿传递
···
data() {
return {
dataProps: {
title: '我是父组件的值',
}
}
}
// communication-sub.vue
<div class="communication-sub">
{{title}}
</div>
···
props: ['title']
// 更推荐开启类型检查
props: {
title: {
type: String,
required: true,
default: '' // 容许指定默认值,引用类型须要函数返回
}
}
···
复制代码
咱们都知道,Props 是单向数据流,这是 Vue 为了不子组件意外改变父组件的状态,从而致使数据流向难以理解而作出的限制。因此 Vue 推荐须要改动的时候,经过改变父组件的值从而触发 Props 的响应。或者,咱们能够在接收非引用类型的值时,使用子组件自身的 data 作一次接收。
props: ['title'],
data: function () {
return {
text: this.title
};
}
复制代码
为何是非引用类型呢,由于在 JavaScript 中,引用类型的赋值,实际是内存地址的传递。因此上面栗子中的简单赋值,显然会指向同一个内存地址,因此若是是数组或是对象,你可能须要一次深拷贝。
let obj = JSON.parse(JSON.stringify(obj));
复制代码
上面这个操做有一些缺陷,不能序列化函数、undefined、循环引用等,详见传送门,可是也能应付一些平常状况了。
事实上,在 Props 是引用类型时,单独修改对象、数组的某个属性或下标,Vue 并不会抛出错误。固然,前提是你要很是清楚本身在作什么,并写好注释,防止你的小伙伴们疑惑。
有的同窗可能知道,在组件上绑定的属性,若是没有在组件内部用 Props 声明,会默认绑定到组件的根元素上去。仍是以前的栗子:
<communication-sub v-bind="dataProps" class="one" type="div"></communication-sub> 复制代码
结果以下:
这是 Vue 默认处理的,并且,除了 class 和 style 采用合并策略,其它特性(如上栗 type)会替换掉原来根元素上的属性值。固然,咱们也能够显示的在组件内部关闭掉这个特性:
...
inheritAttrs: false,
props: ['title']
复制代码
利用 inheritAttrs,咱们还能够方便的把组件绑定的其它特性,转移到咱们指定的元素上。这就须要用到下一个咱们要讲的 $attrs
了。
咱们在使用组件库的时候常常会这么写:
<el-input v-model="input" placeholder="请输入内容"></el-input> 复制代码
实际渲染后:
能够看到咱们指定的的 placeholder 是渲染在 input 上的,可是 input 并非根元素。难道都用 Props 声明后,再赋值给 input?这种状况就能够用到 $attrs
了,改造一下咱们以前那个栗子。
// communication.vue
<template>
<div class="communication">
<communication-sub v-bind="dataProps" class="input" type="text" placeholder="请输入内容">
</communication-sub>
</div>
</template>
<script> import communicationSub from './communication-sub.vue'; export default{ name: 'communication', data() { return { dataProps: { title: '我是 communication 的值', } } }, components: { communicationSub } } </script>
// communication-sub.vue
···
<div class="communication-sub">
<input v-bind="$attrs" v-model="title"></input>
</div>
···
export default {
inheritAttrs: false
}
复制代码
能够看到,type 已经转移到了子元素 input 标签上,可是 class 没有。这是由于 inheritAttrs: false
选项不会影响 style 和 class 的绑定。能够看出 $attrs
则是将没有被组件内部 Props 声明的传值(也叫非 Props 特性)收集起来的一个对象,再经过 v-bind 将其绑定在指定元素上。这也是 Element 等组件库采用的策略。
这里须要注意一点,经过 $attrs 指定给元素的属性,不会与该元素原有属性发生合并或替换,而是以原有属性为准。举个例子,假如我将上述 input 的 type 默认设置为 password。
<input v-bind="$attrs" v-model="title" type="password"></input>
复制代码
则不会采用 $attrs 中的 type: 'text',将以 password 为准,因此若是须要默认值的属性,建议不要用这种方式。
$listeners
同 $attrs
相似,能够看作是一个包含了组件上全部事件监听器(包括自定义事件、不包括.native修饰的事件)的对象。它也支持上述的写法,适用于将事件安放于组件内指定元素上。
// communication.vue
<communication-sub v-bind="dataProps"
class="input"
type="text"
placeholder="请输入内容"
@focus="onFocus" >
</communication-sub> ··· methods: { onFocus() { console.log('onFocus'); } } // communication-sub.vue <input v-bind="$attrs" v-model="title" v-on="$listeners"></input> 复制代码
给以前的栗子绑定一个聚焦事件,在子组件中经过 $listeners
绑定给 input,则会在 input 聚焦时触发。
那么除了用在这种给组件内指定元素绑定特性和事件的状况,还有哪些场景能够用到呢?官方说明:在建立更高层次的组件时很是有用。好比在祖孙组件中传递数据,在孙子组件中触发事件后要在祖辈中作相应更新。咱们继续以前的栗子:在孙辈组件触发点击事件,而后在祖辈中修改相应的 data。
// communication.vue
<communication-sub v-bind="dataProps" @click="onCommunicationClick">
</communication-sub>
···
methods: {
onCommunicationClick() {
this.dataProps.title = '我是点击以后的值';
}
};
// communication-sub.vue
<communication-min-sub v-on="$listeners"></communication-min-sub> // 子组件中将事件透传到孙辈
// communication-min-sub.vue
<template>
<div class="communication-min-sub">
<p>我是 communication-min-sub</p>
<button v-on="$listeners">click</button>
</div>
</template>
<script> export default{ name: 'communication-min-sub', inheritAttrs: false } </script>
复制代码
这样就能很方便的在多级组件的子级组件中,快速访问到父组件的数据和方法。正如在刚才的例子中,button 点击时,是直接调用的 communication.vue 中定义的方法。
上面的方法,在大多数多级组件嵌套的场景颇有用,但有时咱们遇到的并不必定是有父子关系的组件。好比基础组件中的 Select 下拉选择器。
<el-select v-model="value" placeholder="请选择">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> 复制代码
相信你们都使用过上栗或者相似于上栗的基础组件,它们借助 vue 插槽 实现。因此这个时候,el-select 和 el-option 之间的数据通讯,咱们以前的 $attrs
、$listeners
就没有用武之地了。有同窗可能不太理解上面的代码为何要通讯,我简单介绍一下 Element 的处理方式:
咱们能够简单的认为(Element 源码比这个要稍复杂,为了方便理解,简化一下,若有须要,可直接前往源码阅读),在 el-select 中有一个 input 元素,el-option 中是一列渲染好的 li。根据需求,咱们在选中某个 li 的时候,要通知 input 展现相应的数据。并且咱们在实际使用的时候,通常还伴随 el-form、el-form-item等组件,因此迫切须要一种方式:
能够容许一个祖先组件向其全部子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。--- Vue 文档
有同窗可能会想到,这种多级的能够用 Vuex、EventBus等方式,固然能够。只不过咱们如今的前提是基础组件,通常第三方组件库是不会增长一些额外的依赖的。事实上 Vue 自己并不推荐直接在业务中使用 provide、inject,通常在组件、插件库用到的比较多。
可是在项目比较小、业务逻辑比较简单的时候,咱们彻底没必要特地引入 Vuex。只要使用得当,provide、inject 确实不失为一种好办法。说了这么多,咱们来看一下具体用法,咱们将以前的栗子,改成用 provide、inject 来实现。
// communication.vue
<communication-sub v-bind="dataProps" >
</communication-sub>
// @click="onCommunicationClick" 移除以前绑定的时间
···
// 在 provide 添加子代须要接收的方法 onCommunicationClick,
// 也能够直接指定为 this,子代便能访问父代全部的数据和方法。
provide: function () {
return {
onCommunicationClick: this.onCommunicationClick
}
},
methods: {
onCommunicationClick() {
this.dataProps.title = '我是点击以后的值';
}
};
// communication-sub.vue
<communication-min-sub></communication-min-sub>
// 移除以前的 v-on="$listeners",由于在这个组件中不须要用到父组件的方法,因此不用作其它处理
// communication-min-sub.vue
<template>
<div class="communication-min-sub">
···
<button @click="onCommunicationClick">click</button>
// 移除 v-on="$listeners",而后绑定 inject 接收到的方法
</div>
</template>
<script> export default{ name: 'communication-min-sub', inject: ['onCommunicationClick'] // inject 接收父组件的方法 } </script>
复制代码
这种写法和以前的 $listeners 获得的效果是同样的,就再也不放图了。你们能够本身尝试一下,也能够前往源码 webrtc-stream。
思考:有些同窗可能会想到,若是我在根实例,app.vue 中如此设置:
<script> export default { provide () { return { app: this // 设置app为this } }, data () { return { userInfo: null, otherState: null } } } </script>
复制代码
那这样把全部的状态管理都放在 app.data 中,全部的子代中不就能够共享了吗?是否是就不须要 Vuex 了呢?实际上,Vue 自己就提供了一个方法来访问根实例 $root
,因此即便没有 provide 也是能够作到的。那为何不这么用呢?仍是前面提到的缘由,不利于追踪维护,也失去了所谓状态管理的意义。不过,若是你的项目足够小的话,依然能够这么使用。
咱们前面一直说的都是子组件如何触达父组件,那么父组件能不能访问到子组件呢?固然是能够的。
简单来讲就是获取元素的 Dom 对象和子组件实例。若是在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;若是用在子组件上,引用就指向组件实例。获取 Dom 元素就是为了进行一些 Dom 操做,须要注意的一点就是,要在 Dom 加载完成后使用,不然可能获取不到。好比我要将以前 input 的字体颜色改为红色:
<input type="text" v-model="dataProps.title" ref="input">
...
mounted() {
this.$nextTick(_ => { // 确保 Dom 更新完成
this.$refs['input'].style.color = 'red';
});
}
// 这里只是举一个栗子,实际项目中的需求,最好经过 class 的方式,尽可能减小 Dom 操做。
复制代码
那什么状况下须要获取组件实例呢?好比父元素的某个状态改变,须要子组件进行 http 请求更新数据。一般状况下,咱们会选择经过 Props 将状态传递给子组件,而后子组件进行 Watch 监测,若是有变动,则进行相应操做。这个时候,咱们即可以选择使用 ref。
<child ref="child"></child>
···
<script> export default { methods () { onStateChange() { // 变动状态后直接调用子组件方法进行更新 this.$refs['child'].updateData(); } } } </script>
复制代码
$children
、$parent
无独有偶,$children
一样能够完成上面的任务。$children
和 $parent
,顾名思义,一个会找到当前组件的子组件,一个会找到当前组件的父组件。若是有多个子组件,须要依赖组件实例的 name 属性。改写一下上面的方法:
<script>
export default {
methods () {
onStateChange() { // 子组件返回的是一个数组,多个子组件用 $options.name 区分。
this.$children[0].updateData();
}
}
}
</script>
复制代码
$parent
和 $children
用法同样,不过 $parent
返回的父组件实例,不是数组,由于父组件确定只有一个。ref、parent、children 它们几个的一个缺点就是没法处理跨级组件和兄弟组件,后续咱们会介绍 dispatch 和 broadcast 方法,实现跨级通讯。
$emit
,想必你们都很是熟悉,咱们一般用做父子组件间通讯,咱们也叫它自定义事件。$emit
和 $on
都是组件自身的方法,$on
能够监听 $emit
派发的事件,$off
则用来取消事件监听。这也是咱们下一个要讲的通讯方式 EventBus 所依赖的原理。
// 父组件
<template>
<button-component @clickButton="clickButton"></button-component>
// 在父组件利用 v-on 监听
</template>
<script> export default { methods: { clickButton () { ··· } } } </script>
// 子组件
<template>
<button @click="handleClick"></button>
</template>
<script> export default { methods: { handleClick () { // 触发 $emit this.$emit('clickButton'); } }, mounted() { this.$on('clickButton', (...arr) => { // 也能够本身监听 $emit,虽然没什么用··· console.log(...arr); }) } } </script>
复制代码
$emit
的痛点依然是不支持跨级和兄弟组件,Vue 官方推荐咱们使用一个新的 Vue 实例来作一个全局的事件通讯(或者叫中央事件总线···),也就是咱们要讲的 EventBus。了解过的同窗都知道,正常的 bus,咱们通常会挂载到 Vue 的 prototype 上,方便全局调用。
// main.js
Vue.prototype.$bus = new Vue();
复制代码
依旧改写以前的栗子:
<!--communication.vue-->
<communication-sub v-bind="dataProps" >
</communication-sub>
···
beforeDestroy() { <!-- 实例销毁时,须要卸载监听事件 -->
this.$bus.$off('busClick');
},
created() { <!-- 监听子组件触发的 Bus 事件-->
this.$bus.$on('busClick', (data) => {
this.dataProps.title = data;
});
}
<!--communication-min-sub.vue-->
<template>
<div class="communication-min-sub">
<button @click="busClick">click bus</button>
<!--子组件触发点击事件-->
</div>
</template>
<script> export default{ methods: { busClick() { this.$bus.$emit('busClick', 'bus 触发了'); } } } </script>
复制代码
这是一个基础的 EventBus 的实现。如今咱们设想一下,相似于 userInfo 这样的信息,在不少页面都须要用到,那咱们须要在许多页面都作 $on
监听的操做。那可否将这些操做整合到一块儿呢?咱们一块儿来看:
// 新建一个 eventBus.js
import Vue from 'vue';
const bus = new Vue({
data () {
return {
userInfo: {}
}
},
created () {
this.$on('getUserInfo', val => {
this.userInfo = val;
})
}
});
export default bus;
// main.js
import bus from './eventBus';
Vue.prototype.$bus = bus;
// app.vue
methods: {
getUserInfo() {
ajax.post(***).then(data => {
this.$bus.$emit('getUserInfo', data); // 通知 EventBus 更新 userInfo
})
}
}
复制代码
这样在其余页面用到 userInfo 的时候,只须要 this.$bus.userInfo
就能够了。注意刚刚其实没有用 off 卸载掉监听,由于其实 userInfo 这种全局信息,并无一个准确的说要销毁的时机,浏览器关闭的时候,也用不着咱们处理了。可是,若是只是某个页面组件用到的,建议仍是用最开始的方法,在页面销毁的时候卸载掉。
不过反过来说,既然用到了 EventBus,说明状态管理并不复杂,不然仍是建议用 Vuex 来作。最后再给你们推荐一篇文章 Vue中eventbus很头疼?我来帮你,做者处理 EventBus 的思路很巧妙,你们不妨仔细看看。
此部分参考自 Element 源码
在 Vue 1.x 的实现中,有 $dispatch
和 $broadcast
方法,可是在 2.x 被废弃了。$dispatch
的主要做用是向上级组件派发事件,$broadcast
则是向下级广播。它们的优势是都支持跨级,再看一下官方废弃这两个方法的理由:
由于基于组件树结构的事件流方式实在是让人难以理解,而且在组件结构扩展的过程当中会变得愈来愈脆弱。而且
$dispatch
和$broadcast
也没有解决兄弟组件间的通讯问题。
能够看到,主要缘由是在组件结构扩展后不易理解,以及没有解决兄弟组件通讯的问题。可是对于组件库来讲,这依旧是十分有用的,因此它们大多本身实现了这两个方法。对咱们来说,也许在项目中用不到,但学习这种解决问题的思路,是十分必要的。
派发和广播,依赖于组件的 name(最怕此处有人说:若是不写 name,这方法不就没用了?2333···),以此来逐级查找对应的组件实例。Element 的实现中,给全部的组件都加了一个 componentName 属性,因此它是根据 componentName 来查找的。咱们在实现的时候仍是直接用 name。
咱们先来看一下 $dispatch
的简单用法,再来分析思路。
<!--communication-min-sub.vue-->
<template>
<button @click="handleDispatch">dispatch</button>
</template>
<script> import Emitter from '../../utils/emitter'; export default { mixins: [Emitter], // 混入,方便直接调用 methods: { handleDispatch () { this.dispatch('communication', 'onMessage', '触发了dispatch'); } } } </script>
复制代码
<!--communication.vue-->
<script> export default { beforeDestroy() { // 销毁 this.$off('onMessage'); }, mounted () { this.$on('onMessage', (data) => { // 监听 this.dataProps.title = data; }) } } </script>
复制代码
如今明确一下目标,dispatch 方法接收三个参数,组件 name、事件名称、基础数据(可不传)。要作到向上跨级派发事件,须要向上找到指定 name 的组件实例,利用咱们前文提到的 $emit
方法作派送,因此在指定组件就能够用 $on
来监听了。因此 dispatch 本质上就是向上查找到指定组件并触发其自身的 $emit
,以此来作响应,broadcast 则相反。那么如何作到跨级查找呢?
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => { // 遍历全部的 $children
var name = child.$options.name; // 拿到实例的name,Element 此处用的 componentName
if (name === componentName) { // 若是是想要的那个,进行广播
child.$emit.apply(child, [eventName].concat(params));
} else { // 不是则递归查找 直到 $children 为 []
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
// 存在 parent 且 (不存在 name 或 name 和 指定参数不同) 则继续查找
parent = parent.$parent; // 不存在继续取上级
if (parent) {
name = parent.$options.name; // 存在上级 再次赋值并再次循环,进行判断
}
}
if (parent) { // 找到之后 若是有 进行事件派发
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
复制代码
以上是详细的 emitter.js,能够看见,这和咱们以前讲到的 $parent
、$children
、$emit
、$on
都密切相关。这也是为何把它放到后面讲的缘由。以前说过,派发和广播并无解决兄弟组件通讯的问题,因此这里你们也能够拓展思考一下,如何支持兄弟组件间通讯。依然是依赖于$parent
、$children
,能够找到任意指定组件。
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的全部组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。--- 官方文档
Vuex 相信你们都比较熟悉了,我不打算在这里把 API 再演示一遍。由于我以为,官方文档 已经很是详细了。Vuex 的核心是单向数据流,并以相应规则保证全部的状态管理均可追踪、可预测。
咱们须要知道何时该用 Vuex,若是你的项目比较小,状态管理比较简单,彻底没有必要使用 Vuex,你能够考虑咱们前文提到的几种方式。
本期文章内容到这里就讲完了,咱们来总结回顾一下:
$parent
、$attrs
、$listeners
、provide 和 inject、$dispatch
$emit
和 $on
、$children
、$ref
、broadcast
原本想按照是否支持跨级来分,可是这里的界定比较模糊:若是逐级传递,有些也能作到跨级,但这并非咱们想要的。因此咱们只要本身清楚在什么状况下该怎么用就行了。
qq前端交流群:960807765,欢迎各类技术交流,期待你的加入
若是你看到了这里,且本文对你有一点帮助的话,但愿你能够动动小手支持一下做者,感谢🍻。文中若有不对之处,也欢迎你们指出,共勉。
更多文章:
欢迎关注公众号 前端发动机,第一时间得到做者文章推送,还有各种前端优质文章,但愿在将来的前端路上,与你一同成长。