【2019 前端进阶之路】Vue 组件间通讯方式完整版

前言

Vue.js 在现今使用有多普遍不用多说,而 Vue 的一大特色就是组件化。本期要讲的,即是 Vue 组件间通讯方式的总结,这也几乎是近年 Vue 面试中的必考题。注:文中示例都基于 Vue 脚手架讲解,会用到一些 Element UI 示例。javascript

【前端进阶之路】会做为一个新系列连载,后续会更多优质前端内容,感兴趣的同窗不妨关注一下。 文章最后有 交流群公众号,能够一块儿学习交流,感谢🍻。html

  • 下期预告:深刻 Vue 响应式原理,手写一个 mvvm

组件

组件是能够复用的 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

  • 页面组件:咱们使用 Vue 时,每一个路由表明的页面,均可以称之为组件。
  • 基础组件:就像上面栗子中的 Icon 组件,就是一个典型的基础组件。基本上不掺杂业务逻辑,在项目中可能被大量使用,易于移植。相似的基础组件还有 Button、Input 等,常见于各种 UI 组件库。
  • 业务组件:业务组件和项目具体的业务逻辑有大量耦合,通常抽离于当前项目。

以上就是组件的简单介绍,那咱们到底为何要推崇组件化?组件化有什么好处?复用?我我的认为组件化最大的好处,即是解耦,易于项目管理。因此在大型项目管理中,组件化是很是有必要的。固然,这并非今天学习的重点,之后有机会再聊。webpack

正由于在 Vue 中到处都是组件,而咱们也偏向于组件化、模块化。那咱们在一堆组件中,便须要解决一个问题 — 组件间通讯。下面,咱们就进入今天的主题,Vue 的组件间通讯。git

组件间通讯

组件间通讯是咱们在 Vue 项目中不可避免的问题,深入了解了 Vue 组件间通讯的几种方式,才能让咱们在处理各类交互问题时游刃有余。github

Props

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 了。

attrs、listeners

咱们在使用组件库的时候常常会这么写:

<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 中定义的方法。

依赖注入 provide、inject

上面的方法,在大多数多级组件嵌套的场景颇有用,但有时咱们遇到的并不必定是有父子关系的组件。好比基础组件中的 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 也是能够作到的。那为何不这么用呢?仍是前面提到的缘由,不利于追踪维护,也失去了所谓状态管理的意义。不过,若是你的项目足够小的话,依然能够这么使用。

ref、parent、children

咱们前面一直说的都是子组件如何触达父组件,那么父组件能不能访问到子组件呢?固然是能够的。

  • ref

简单来讲就是获取元素的 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、on、off

$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>
复制代码

EventBus

$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 的思路很巧妙,你们不妨仔细看看。

派发与广播:dispatch 与 broadcast

此部分参考自 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

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的全部组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。--- 官方文档

Vuex 相信你们都比较熟悉了,我不打算在这里把 API 再演示一遍。由于我以为,官方文档 已经很是详细了。Vuex 的核心是单向数据流,并以相应规则保证全部的状态管理均可追踪、可预测。

咱们须要知道何时该用 Vuex,若是你的项目比较小,状态管理比较简单,彻底没有必要使用 Vuex,你能够考虑咱们前文提到的几种方式。

总结

本期文章内容到这里就讲完了,咱们来总结回顾一下:

  • 子组件触达父组件的方式:Props、$parent$attrs$listeners、provide 和 inject、$dispatch
  • 父组件触达子组件的方式:$emit$on$children$refbroadcast
  • 全局通讯:EventBus、Vuex

原本想按照是否支持跨级来分,可是这里的界定比较模糊:若是逐级传递,有些也能作到跨级,但这并非咱们想要的。因此咱们只要本身清楚在什么状况下该怎么用就行了。

交流群

qq前端交流群:960807765,欢迎各类技术交流,期待你的加入

后记

若是你看到了这里,且本文对你有一点帮助的话,但愿你能够动动小手支持一下做者,感谢🍻。文中若有不对之处,也欢迎你们指出,共勉。

更多文章:

欢迎关注公众号 前端发动机,第一时间得到做者文章推送,还有各种前端优质文章,但愿在将来的前端路上,与你一同成长。

相关文章
相关标签/搜索