cover_image

Vue 组件通讯实践

37MOYU_XXB 三七互娱技术团队 2019年12月10日 10:00

前言

组件是 vue.js 最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。组件一般有以下几种关系:

图片


如上图所示,grandfather 和 parent、parent 和 childA、parent 和 childB 都是父子关系,childA 和 childB 是兄弟关系,grandfather 和 childA、childB 是隔代关系(可能隔多代)。 所以组件通讯是 vue.js 的核心之一,接下来结合代码,来了解各个组件是怎么通讯的。

1

props 和 $emit

子组件(Child.vue)的 props 属性能够接收来自父组件(Parent.vue)数据。没错,仅仅只能接收,props是单向绑定的,即只能父组件向子组件传递,不能反向。
// 父组件(Parent.vue)<template>    <div id="parent">        <Child :msg="msg" />    </div></template>
<script>import Child from './Child'
export default { name: 'parent', data() { return { msg: '这是来自父组件来的数据~~' } },
components: { Child }}</script>// 子组件(Child.vue)<template> <div id="child"> <div>{{ msg }}</div> </div></template>
<script>export default { name: 'child', data() { return { }    },     props: { msg: { type: String } },
methods: { }}</script>
$emit 实现子组件向父组件传值(通过事件形式),子组件通过 $emit 事件向父组件发送消息,将自己的数据传递给父组件。
// 父组件<template>    <div id="parent">        <div>{{ msg }}</div>        <Child2 @changeMsg="parentMsg" />    </div></template>
<script>import Child2 from './Child2'
export default { name: 'parent', data() { return { msg: '' } },
methods: { parentMsg( msg ) { this.msg = msg; } },
components: { Child2 }}</script>// 子组件<template> <div id="child"> <button @click="childMsg">传递数据给父组件</button> </div></template>
<script>export default { name: 'child', data() { return { } },
methods: { childMsg() { this.$emit( 'changeMsg', '传递数据给粑粑组件' ); } }}</script>

总结:开发组件常用的数据传输方式,父子间传递。


2

$emit 和 $on

实现方式是通过创建一个空的 vue 实例,当做 $emit 事件的处理中心(事件总线),通过它来触发以及监听事件,来实现任意组件间的通信,包含父子,兄弟,隔代组件。
// 父组件<template>    <div id="parent">        <Child1 :Event="Event" />        <Child2 :Event="Event" />        <Child3 :Event="Event" />    </div></template>
<script>import Vue from 'Vue';import Child1 from './Child1';import Child2 from './Child2';import Child3 from './Child3';
// 公共的实例const Event = new Vue();
export default { name: 'parent', data() { return { Event } },
components: { Child1, Child2, Child3 }}</script>// 子组件1<template> <div id="child1"> 1、她的名字叫:{{ name }} <button @click="send">传递数据给Child3</button> </div></template>
<script>export default { name: 'child1', data() { return { name: '柯基慧' } },
props: { Event: Object },
methods: { send() { this.Event.$emit( 'msgA', this.name ); } }}</script>// 子组件2<template> <div id="child2"> 1、她的身高:{{ height }} <button @click="send">传递数据给Child3</button> </div></template>
<script>export default { name: 'child2', data() { return { height: '149.9cm' } },
props: { Event: Object },
methods: { send() { this.Event.$emit( 'msgB', this.height ); } }}</script>// 子组件3<template> <div id="child3"> <h3>她的名字叫:{{ name }},身高{{ height }}。</h3> </div></template>
<script>export default { name: 'child3', data() { return { name: '', height: '' } },
props: { Event: Object },
mounted() { this.Event.$on( 'msgA', name => { this.name = name; } );
this.Event.$on( 'msgB', height => { this.height = height; } ); }}</script>

总结:在父子、兄弟、隔代组件中都可以互相数据通信,重要的是 $emit 和 $on 事件必须是在一个公共的实例上才能触发。


3

$attrs 和 $listeners

Vue 组件间传输数据在 Vue2.4 版本后增加了新方法 $attrs 和 $listeners 。

1)$attrs

$attrs - 包含了父作用域中不作为 props 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。当一个组件没有声明任何 props 时,这里会包含所有父作用域的绑定 ( class 和style 除外),并且可以通过 v-bind="$attrs" 传入内部组件 - 在创建高级别的组件时非常有用。 简单点讲就是包含了所以父组件在子组件上设置的属性(除了 props 传递的属性、class 和 style )。
想象一下,你打算封装一个自定义input组件 - MyInput,需要从父组件传入 type,placeholder,title 等多个html元素的原生属性。此时你的 MyInput 组件 props 如下:
props:['type', 'placeholder', 'title', ...]
如果它的属性越多,那子组件就要定义更多的属性,会很影响阅读,所以,$attrs 专门为了解决这种问题而诞生,这个属性允许你在使用自定义组件时更像是使用原生 html 元素。比如:
// 父组件<template>    <div id="parentAttrs">        <MyInput placeholder="请输入你的姓名" type="text" title="姓名" v-model="name" />    </div></template>
<script>import MyInput from './MyInput';
export default { name: 'parent', data() { return { name: '' } },
components: { MyInput }}</script>// 子组件<template> <div> <label>姓名:</label> <input v-bind="$attrsAll" @input="$emit( 'input', $event.target.value )" /> </div></template><script>export default { name: 'myinput', data() { return {} },
inheritAttrs: false,
computed: { $attrsAll() { return { value: this.$vnode.data.model.value, ...this.$attrs } } }}</script>

2)$listener

$listeners - 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件 - 在创建更高层次的组件时非常有用。 简单点讲它是一个对象,里面包含了作用在这个组件上所有的监听器(监听事件),可以通过 v-on="$listeners" 将事件监听指向这个组件内的子元素(包括内部的子组件)。
同上面 $attrs 属性一样,这个属性也是为了在自定义组件中使用原生事件而产生的。比如要让前面的 MyInput 组件实现 focus 事件,直接这么写是没用的。
<template>    <div id="parentListener">        <MyInput @focus="focus" placeholder="请输入你的姓名" type="text" title="姓名" v-model="name" />    </div></template>
<script>import MyInput from './MyInput';
export default { name: 'parent', data() { return { name: '' } },
methods: { focus() { console.log( 'test' ); } },
components: { MyInput }}</script>

必须要让 focus 事件作用于 MyInput 组件的 input 元素上。

<template>    <div>        <label>姓名:</label>        <input v-bind="$attrsAll" v-on="$listenserAll" />        <button @click="handlerF">操作test</button>    </div></template><script>export default {    name: 'myinput',    data() {        return {}    },
inheritAttrs: false,
props: ['value'],
methods: { handlerF() { this.$emit( 'focus' ); } },
computed:{ $attrsAll() { return { value: this.value, ...this.$attrs } },
$listenserAll() { return Object.assign( {}, this.$listeners, {input: event => this.$emit( 'input', event.target.value )}) } }}</script>
$attrs 里存放的是父组件中绑定的非 props 属性,$listeners 里面存放的是父组件中绑定的非原生事件。组件可以通过在自己的子组件上使用 v-on=”$listeners”,进一步把值传给自己的子组件。如果子组件已经绑定 $listener 中同名的监听器,则两个监听器函数会以冒泡的方式先后执行。

总结:用在父组件传递数据给子组件或者孙组件。


4

provide 和 inject

Vue2.2 版本以后新增了这两个 API, 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
使用方法:provide 在父组件中返回要传给下级的数据;inject 在需要使用这个数据的子辈组件或者孙辈等下级组件中注入数据。
使用场景:由于 vue 有 $parent 属性可以让子组件访问父组件。但孙组件想要访问祖先组件就比较困难。通过 provide/inject 可以轻松实现跨级访问父组件的数据。
注意:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
// 父组件<template>    <div class="parentProvide">        <button @click="changeSth">我要干嘛好呢~</button>        <p>要干嘛:{{ sth }}</p>        <ChildA />    </div></template>
<script>import ChildA from './ChildA';
export default { name: 'parent-pro', data() { return { sth: '吃饭~' } },
// 在父组件传入变量 provide() { return { obj: this } },
methods: { changeSth() { this.sth = '睡觉~'; } },
components: { ChildA }}</script>// 子组件A<template> <div> <div class="childA"> <p>子组件A该干嘛呢:{{ this.obj.sth }}</p> </div> <ChildB /> </div></template>
<script>import ChildB from "./ChildB";
export default { name: "child-a", data() { return {}; },
props: {},
// 在子组件拿到变量 inject: { obj: { default: () => { return {} } } },
components: { ChildB }}</script>// 子组件B<template> <div> <div class="childB"> <p>子组件B该干嘛呢:{{ this.obj.sth }}</p> </div> </div></template>
<script>export default { name: "child-b", data() { return {}; },
props: {},
// 在子组件拿到变量 inject: { obj: { default: () => { return {} } } }}</script

总结:传输数据父级一次注入,子孙组件一起共享的方式。


5

$parent 和 $children & $refs

$parent 和 $children :指定已创建的实例之父实例,在两者之间建立父子关系。子实例可以用 this.$parent 访问父实例,子实例被推入父实例的 $children 数组中。
$refs :一个对象,持有注册过 ref 特性的所有 DOM 元素和组件实例。ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件。
// 父组件<template>    <div class="parentPC">        <p>我的名字:{{ name }}</p>        <p>我的标题:{{ title }}</p>        <ChildA ref="comp1" />        <ChildB ref="comp2" />    </div></template>
<script>import ChildA from "./ChildA.vue";import ChildB from "./ChildB.vue";export default { name: 'parent-pc', data() { return { name: '', title: '', contentToA: 'parent-pc-to-A', contentToB: 'parent-pc-to-B' } },
mounted() { const comp1 = this.$refs.comp1; this.title = comp1.title; comp1.sayHi(); this.name = this.$children[1].title; },
components: { ChildA, ChildB }}</script>// 子组件A - ref方式<template> <div> <p>(ChildA)我的父组件是谁:{{ content }}</p> </div></template>
<script>export default { name: 'child-a', data() { return { title: '我是子组件child-a', content: '' } },
methods: { sayHi() { console.log( 'Hi, girl~' ); } },
mounted() { this.content = this.$parent.contentToA; }}</script>// 子组件B - children方式<template> <div> <p>(ChildB)我的父组件是谁:{{ content }}</p> </div></template>
<script>export default { name: 'child-b', data() { return { title: '我是子组件child-b', content: '' } },
mounted() { this.content = this.$parent.contentToB; }}</script>
从上面例子可以看到这两种方式都可以父子间通信,而缺点就是都不能跨级以及兄弟间通信。

总结:父子组件间共享数据以及方法的便捷实践之一。


6

Vuex

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

图片

Vuex 实现了一个单项数据流,通过创建一个全局的 State 数据,组件想要修改 State 数据只能通过 Mutation 来进行,例如页面上的操作想要修改 State 数据时,需要通过 Dispatch (触发 Action ),而 Action 也不能直接操作数据,还需要通过 Mutation 来修改 State 中数据,最后根据 State 中数据的变化,来渲染页面。

1、State (index.js)

State 用来存状态。在根实例中注册了 store 后,用 this.$store.state 来访问。
Vue.use(Vuex);
const state = { userInfo: {}, // 用户信息};
export default new Vuex.Store({ state, getters, mutations, actions});

2、Getters

Getters 从 State 上派生出来的状态。可以理解为基于 State 的计算属性。很多时候,不需要 Getters,直接用 State 即可。
export default {    /**    @description    获取用户信息    */    getUserInfo( states ) {        return states.userInfo;    }}

3、Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 Mutation。

Mutation 用来改变状态。需要注意的是,Mutation 里的修改状态的操作必须是同步的。在根实例中注册了 store 后, 可以用 this.$store.commit('xxx', data) 来通知 Mutation 来改状态。

export const UPDATE_USERINFO = "UPDATE_USERINFO";export default {    [type.UPDATE_USERINFO]( states, obj ) {        states.userInfo = obj;    }}

4、Action

  • Action 提交的是 Mutation,而不是直接变更状态。

  • Action 可以包含任意异步操作。

在根实例中注册了 store 后, 可以用 this.$store.dispatch('xxx', data) 来存触发 Action。

export default {    update_userinfo({        commit    }, param) {        commit( "UPDATE_USERINFO", param );    }}
乍一眼看上去感觉多此一举,我们直接分发 Mutation 岂不更方便?实际上并非如此,还记得 Mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 Action 内部执行异步操作:
actions: {  incrementAsync ({ commit }) {    setTimeout(() => {      commit('increment')    }, 1000)  }}

总结:对 Vue 应用中多个组件的共享状态进行集中式的管理(读/写),统一的维护了一份共同的 State 数据,方便组件间共同调用。


7

slot-scope 和 v-slot

从 vue@2.6.x 开始,Vue 为具名和范围插槽引入了一个全新的语法,v-slot 指令。
一个假设的 <base-layout> 组件的模板如下:
<template>    <div class="container">        <header>            <slot name="header"></slot>        </header>
<main> <slot></slot> </main>
<footer> <slot name="footer"></slot> </footer> </div></template>
<script>export default { name: "base-layout", data() { return {} }}</script>
在向具名插槽提供内容的时候,我们可以在一个父组件的 <template> 元素上使用 v-slot 特性:
// 父组件<template>    <base-layout>        <template v-slot:header>            <h1>Here might be a page title</h1>        </template>
<p>A paragraph for the main content.</p> <p>And another one.</p>
<template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout></template>
<script>import BaseLayout from "./BaseLayout";
export default { name: "parent-slot", data() { return { } },
components: { BaseLayout }}</script>

    插槽的名字现在通过 v-slot:slotName 这种形式来使用,没有名字的 <slot> 隐含有一个 "default" 名称:

<template v-slot:default>    <p>A paragraph for the main content.</p>    <p>And another one.</p></template>


8

scopedSlots 属性

scopedSlots 是编程式语法,在 render() 函数中使用 scopedSlots。

// baseLayout.vue<script>export default {    data() {        return {            headerText: "child header text",            defaultText: "child default text",            footerText: "child footer text"        }    },
render( h ) { return h("div", { class: "child-node" }, [ this.$scopedSlots.header({ text: this.headerText }), this.$scopedSlots.default(this.defaultText), this.$scopedSlots.footer({ text: this.footerText }) ]); }}</script><script>import BaseLayout from "./baseLayout";export default { name: "ScopedSlots", components: { BaseLayout },
render(h) { return h("div", { class: "parent-node" }, [ this.$slots.default, h("base-layout", { scopedSlots: { header: props => { return h("p", { style: { color: "red" } }, [ props.text ]); },
default: props => { return h("p", { style: { color: "deeppink" } }, [ props ]); },
footer: props => { return h("p", { style: { color: "orange" } }, [ props.text ]); } } }) ]); }}</script>


总结

组件间不同的使用场景可以分为 3 类,对应的通信方式如下:
父子通信:props 和 $emit,$emit 和 $on,Vuex,$attrs 和 $listeners,provide 和 inject,$parent 和 $children&$refs
兄弟通信:$emit 和 $on,Vuex
隔代(跨级)通信:$emit 和 $on,Vuex,provide 和 inject,$attrs 和 $listeners

排 版 | chuanrui
如下技术文章,你可能也感兴趣:
从实践中理解IO模型
前端动效库搭建之路
定时任务管理之批量管理
从删库到不跑路(下)-之变更回滚


图片

图片

微信扫一扫
关注该公众号

继续滑动看下一个
三七互娱技术团队
向上滑动看下一个