Vue 组件数据通信方案总结

前言

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

组件关系图

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

一、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>

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

二、$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 事件必须是在一个公共的实例上才能触发。

三、$attrs$listeners

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

$attrs

$attrs - 包含了父作用域中不作为 props 被识别 (且获取) 的特性绑定 ( classstyle 除外)。当一个组件没有声明任何 props 时,这里会包含所有父作用域的绑定 ( classstyle 除外),并且可以通过 v-bind="$attrs" 传入内部组件 - 在创建高级别的组件时非常有用。 简单点讲就是包含了所以父组件在子组件上设置的属性(除了 props 传递的属性、classstyle )。

想象一下,你打算封装一个自定义input组件 - MyInput,需要从父组件传入 typeplaceholdertitle 等多个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>

$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 中同名的监听器,则两个监听器函数会以冒泡的方式先后执行。

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

四、provideinject

Vue2.2 版本以后新增了这两个 API, 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。

使用方法:provide 在父组件中返回要传给下级的数据;inject 在需要使用这个数据的子辈组件或者孙辈等下级组件中注入数据。

使用场景:由于 vue$parent 属性可以让子组件访问父组件。但孙组件想要访问祖先组件就比较困难。通过 provide/inject 可以轻松实现跨级访问父组件的数据。

注意:provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

// 父组件
<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>

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

五、$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>

从上面例子可以看到这两种方式都可以父子间通信,而缺点就是都不能跨级以及兄弟间通信。

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

六、Vuex

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

Vuex

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

GettersState 上派生出来的状态。可以理解为基于 State 的计算属性。很多时候,不需要 Getters,直接用 State 即可。

export default {
    /**
    @description    获取用户信息
    */
    getUserInfo( states ) {
        return states.userInfo;
    }
}

3、Mutation

更改 Vuexstore 中的状态的唯一方法是提交 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 数据,方便组件间共同调用。

七、slot-scopev-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>

八、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$onVuex$attrs$listenersprovideinject$parent$children$refs

兄弟通信$emit$onVuex

隔代(跨级)通信$emit$onVuexprovideinject$attrs$listeners

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336

推荐阅读更多精彩内容