百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

vue 组件通信看这篇就够了(12种通信方式)

wptr33 2024-11-14 19:21 20 浏览


vue 组件间的通信是 vue 开发中很基础也十分重要的部分,作为使用 vue 的开发者每天都在使用。同时,vue 通信也是面试中非常高频的问题,有很多面试题,都是围绕通信展开。

本文会介绍常见的通信方式,并分析每种方式的使用场景和注意点。

vue中提倡单向数据流,这是为了保证数据流向的简洁性,使程序更易于理解。但对于一些边界情况,vue也提供了隐性的通信方式,这些通信方式会打破单向数据流的原则,应该谨慎使用。

下面我们将组件通信分为父子组件通信 和 非父子组件通信进行分析。

父子组件通信

prop 和 events

prop 和 events 最基础也最常用,这里不提供示例。

通过 prop 向下传递,通过事件向上传递是一个 vue 项目最理想的通信状态。

使用时有两点需要注意:

第一,不应该在一个子组件内部改变 prop,这样会破坏单向的数据绑定,导致数据流难以理解。如果有这样的需要,可以通过 data 属性接收或使用 computed 属性进行转换。

第二,如果 props 传递的是引用类型(对象或者数组),在子组件中改变这个对象或数组,父组件的状态会也会做相应的更新,利用这一点就能够实现父子组件数据的“双向绑定”,虽然这样实现能够节省代码,但会牺牲数据流向的简洁性,令人难以理解,最好不要这样去做。想要实现父子组件的数据“双向绑定”,可以使用 v-model 或 .sync。

v-model 指令

v-model 是用来在表单控件或者组件上创建双向绑定的,他的本质是 v-bind 和 v-on 的语法糖,在一个组件上使用 v-model,默认会为组件绑定名为 value 的 prop 和名为 input 的事件。

当我们组件中的某一个 prop 需要实现上面所说的”双向绑定“时,v-model 就能大显身手了。有了它,就不需要自己手动在组件上绑定监听当前实例上的自定义事件,会使代码更简洁。

下面以一个 input 组件实现的核心代码,介绍下 v-model 的应用。


<!--父组件-->
<template>
 <base-input v-model="input"></base-input>
</template>
<script>
 export default {
 data() {
 return {
 input: ''
 }
 },
 }
</script>

<!--子组件-->
<template>
 <input type="text" :value="currentValue" @input="handleInput">
</template>
<script>
 export default {
 data() {
 return {
 currentValue: this.value === undefined || this.value === null ? ''
 }
 },
 props: {
 value: [String, Number],
 },
 methods: {
 handleInput(event) {
 const value = event.target.value;
 this.$emit('input', value);
 },
 },
}
</script>

有时,在某些特定的控件中名为 value 的属性会有特殊的含义,这时可以通过 model 选项来回避这种冲突。

.sync 修饰符

.sync 修饰符在 vue 1.x 的版本中就已经提供,1.x 版本中,当子组件改变了一个带有 .sync 的 prop 的值时,会将这个值同步到父组件中的值。这样使用起来十分方便,但问题也十分明显,这样破坏了单向数据流,当应用复杂时,debug 的成本会非常高。

关注我的头条号,分享更多的技术学习文章,我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。

在vue 2.0中移除了 .sync。但是在实际的应用中,.sync 是有它的应用场景的,所以在 vue 2.3 版本中,又迎来了全新的 .sync。

新的 .sync 修饰符所实现的已经不再是真正的双向绑定,它的本质和 v-model 类似,只是一种缩写。


<text-document
 v-bind:title="doc.title"
 v-on:update:title="doc.title = $event"
></text-document>

上面的代码,使用 .sync 就可以写成


<text-document v-bind:title.sync="doc.title"></text-document>

这样,在子组件中,就可以通过下面代码来实现对这个 prop 重新赋值的意图了。


this.$emit('update:title', newTitle)

v-model 和 .sync 对比

.sync 从功能上看和 v-model 十分相似,都是为了实现数据的“双向绑定”,本质上,也都不是真正的双向绑定,而是语法糖。

相比较之下,.sync 更加灵活,它可以给多个 prop 使用,而 v-model 在一个组件中只能有一个。

从语义上来看,v-model 绑定的值是指这个组件的绑定值,比如 input 组件,select 组件,日期时间选择组件,颜色选择器组件,这些组件所绑定的值使用 v-model 比较合适。其他情况,没有这种语义,个人认为使用 .sync 更好。

ref

ref 特性可以为子组件赋予一个 ID 引用,通过这个 ID 引用可以直接访问这个子组件的实例。当父组件中需要主动获取子组件中的数据或者方法时,可以使用 $ref 来获取。


<!--父组件-->
<template>
 <base-input ref="baseInput"></base-input>
</template>
<script>
 export default {
 methods: {
 focusInput: function () {
 this.$refs.usernameInput.focus()
 }
 }
}
</script>

<!--子组件-->
<template>
 <input ref="input">
</template>
<script>
 export default {
 methods: {
 focus: function () {
 this.$refs.input.focus()
 }
 }
}
</script>

使用 ref 时,有两点需要注意

  • $refs 是作为渲染结果被创建的,所以在初始渲染的时候它还不存在,此时无法无法访问。
  • $refs 不是响应式的,只能拿到获取它的那一刻子组件实例的状态,所以要避免在模板和计算属性中使用它。

$parent 和 $children

$parent 属性可以用来从一个子组件访问父组件的实例,$children 属性 可以获取当前实例的直接子组件。

看起来使用 $parent 比使用prop传值更加简单灵活,可以随时获取父组件的数据或方法,又不像使用 prop 那样需要提前定义好。但使用 $parent 会导致父组件数据变更后,很难去定位这个变更是从哪里发起的,所以在绝大多数情况下,不推荐使用。

在有些场景下,两个组件之间可能是父子关系,也可能是更多层嵌套的祖孙关系,这时就可以使用 $parent。

下面是 element ui 中的组件 el-radio-group 和 组件 el-radio 使用示例:


<template>
 <el-radio-group v-model="radio1">
 <el-radio :label="3">备选项</el-radio>
 <component-1>
 <el-radio :label="3">备选项</el-radio>
 </component-1>
 </el-radio-group>
</template>
<script>
 export default {
 data () {
 return {
 radio2: 3
 };
 }
 }
</script>

在 el-radio-group 和 组件 el-radio 通信中, 组件 el-radio 的 value 值需要和 el-radio-group的 v-model 的值进行“绑定”,我们就可以在 el-radio 内借助 $parent 来访问到 el-radio-group 的实例,来获取到 el-radio-group 中 v-model 绑定的值。

下面是获取 el-radio 组件中获取 el-radio-group 实例的源码:


// el-radio组件
 let parent = this.$parent;
 while (parent) {
 if (parent.$options.componentName !== 'ElRadioGroup') {
 parent = parent.$parent;
 } else {
 this._radioGroup = parent; // this._radioGroup 为组件 el-radio-group 的实例
 }
 }

非父子组件通信

$attrs 和 $listeners

当要和一个嵌套很深的组件进行通信时,如果使用 prop 和 events 就会显的十分繁琐,中间的组件只起到了一个中转站的作用,像下面这样:


<!--父组件-->
 <parent-component :message="message">我是父组件</parent-component>
<!--子组件-->
 <child-component :message="message">我是子组件</child-component>
<!--孙子组件-->
 <grand-child-component :message="message">我是孙子组件</grand-child-component>

当要传递的数据很多时,就需要在中间的每个组件都重复写很多遍,反过来从后代组件向祖先组件使用 events 传递也会有同样的问题。使用 $attrs 和 $listeners 就可以简化这样的写法。

$attrs 会包含父组件中没有被 prop 接收的所有属性(不包含class 和 style 属性),可以通过 v-bind="$attrs" 直接将这些属性传入内部组件。

$listeners 会包含所有父组件中的 v-on 事件监听器 (不包含 .native 修饰器的) ,可以通过 v-on="$listeners" 传入内部组件。

下面以父组件和孙子组件的通信为例介绍它们的使用:


<!--父组件 parent.vue-->
<template>
 <child :name="name" :message="message" @sayHello="sayHello"></child>
</template>
<script>
export default {
 inheritAttrs: false,
 data() {
 return {
 name: '通信',
 message: 'Hi',
 }
 },
 methods: {
 sayHello(mes) {
 console.log('mes', mes) // => "hello"
 },
 },
}
</script>

<!--子组件 child.vue-->
<template>
 <grandchild v-bind="$attrs" v-on="$listeners"></grandchild>
</template>
<script>
export default {
 data() {
 return {}
 },
 props: {
 name,
 },
}
</script>

<!--孙子组件 grand-child.vue-->
<template>
</template>
<script>
export default {
 created() {
 this.$emit('sayHello', 'hello')
 },
}
</script>

provide 和 inject

provide 和 inject 需要在一起使用,它可以使一个祖先组件向其所有子孙后代注入一个依赖,可以指定想要提供给后代组件的数据/方法,不论组件层次有多深,都能够使用。


<!--祖先组件-->
<script>
export default {
 provide: {
 author: 'yushihu',
 },
 data() {},
}
</script>

<!--子孙组件-->
<script>
export default {
 inject: ['author'],
 created() {
 console.log('author', this.author) // => yushihu
 },
}
</script>

provide 和 inject 绑定不是响应的,它被设计是为组件库和高阶组件服务的,平常业务中的代码不建议使用。

dispatch 和 broadcast

vue 在2.0版本就已经移除了 $dispatch 和 $broadcast,因为这种基于组件树结构的事件流方式会在组件结构扩展的过程中会变得越来越难维护。但在某些不使用 vuex 的情况下,仍然有使用它们的场景。所以 element ui 和 iview 等开源组件库中对 broadcast 和 dispatch 方法进行了重写,并通过 mixin 的方式植入到每个组件中。

实现 dispatch 和 broadcast 主要利用我们上面已经说过的 $parent 和 $children。


//element ui 中重写 broadcast 的源码
function broadcast(componentName, eventName, params) {
 this.$children.forEach(child => {
 var name = child.$options.componentName;
 if (name === componentName) {
 child.$emit.apply(child, [eventName].concat(params));
 } else {
 broadcast.apply(child, [componentName, eventName].concat([params]));
 }
 });
}

broadcast 方法的作用是向后代组件传值,它会遍历所有的后代组件,如果后代组件的 componentName 与当前的组件名一致,则触发 $emit 事件,将数据 params 传给它。


 //element ui 中重写 dispatch 的源码
 dispatch(componentName, eventName, params) {
 var parent = this.$parent || this.$root;
 var name = parent.$options.componentName;
 while (parent && (!name || name !== componentName)) {
 parent = parent.$parent;
 if (parent) {
 name = parent.$options.componentName;
 }
 }
 if (parent) {
 parent.$emit.apply(parent, [eventName].concat(params));
 }
 }

dispatch 的作用是向祖先组件传值,它会一直寻找父组件,直到找到组件名和当前传入的组件名一致的祖先组件,就会触发其身上的 $emit 事件,将数据传给它。这个寻找对应的父组件的过程和文章前面讲解 $parent 的例子类似。

eventBus

对于比较小型的项目,没有必要引入 vuex 的情况下,可以使用 eventBus。相比我们上面说的所有通信方式,eventBus 可以实现任意两个组件间的通信。

它的实现思想也很好理解,在要相互通信的两个组件中,都引入同一个新的vue实例,然后在两个组件中通过分别调用这个实例的事件触发和监听来实现通信。


//eventBus.js
import Vue from 'vue';
export default new Vue();

<!--组件A-->
<script>
import Bus from 'eventBus.js';
export default {
 methods: {
 sayHello() {
 Bus.$emit('sayHello', 'hello');
 }
 }
}
</script>

<!--组件B-->
<script>
import Bus from 'eventBus.js';
export default {
 created() {
 Bus.$on('sayHello', target => {
 console.log(target); // => 'hello'
 });
 }
}
</script>

通过 $root 访问根实例

通过 $root,任何组件都可以获取当前组件树的根 Vue 实例,通过维护根实例上的 data,就可以实现组件间的数据共享。


//main.js 根实例
new Vue({
 el: '#app',
 store,
 router,
 // 根实例的 data 属性,维护通用的数据
 data: function () {
 return {
 author: ''
 }
 },
 components: { App },
 template: '<App/>',
});

<!--组件A-->
<script>
export default {
 created() {
 this.$root.author = '于是乎'
 }
}
</script>

<!--组件B-->
<template>
 <div><span>本文作者</span>{{ $root.author }}</div>
</template>

通过这种方式,虽然可以实现通信,但在应用的任何部分,任何时间发生的任何数据变化,都不会留下变更的记录,这对于稍复杂的应用来说,调试是致命的,不建议在实际应用中使用。

Vuex

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

使用 Vuex 并不代表就要把所有的状态放入 Vuex 管理,这样做会让代码变的冗长,无法直观的看出要做什么。对于严格属于组件私有的状态还是应该在组件内部管理更好。

自己实现简单的 Store 模式

对于小型的项目,通信十分简单,这时使用 Vuex 反而会显得冗余和繁琐,这种情况最好不要使用 Vuex,可以自己在项目中实现简单的 Store。


//store.js
var store = {
 debug: true,
 state: {
 author: 'yushihu!'
 },
 setAuthorAction (newValue) {
 if (this.debug) console.log('setAuthorAction triggered with', newValue)
 this.state.author = newValue
 },
 deleteAuthorAction () {
 if (this.debug) console.log('deleteAuthorAction triggered')
 this.state.author = ''
 }
}

和 Vuex 一样,store 中 state 的改变都由 store 内部的 action 来触发,并且能够通过 log 保留触发的痕迹。这种方式十分适合在不需要使用 Vuex 的小项目中应用。

与 $root 访问根实例的方法相比,这种集中式状态管理的方式能够在调试过程中,通过 log 记录来确定当前变化是如何触发的,更容易定位问题。

相关推荐

C++企业级开发规范指南(c++开发gui)

打造高质量、可维护的C++代码标准一、前言C++作为一门功能强大的系统级编程语言,被广泛应用于操作系统、游戏引擎、高性能服务器、数据库系统等领域。知名互联网公司(如Google、Microsoft、腾...

C++|整型的最值、上溢、下溢、截断、类型提升和转换

整数在计算机内以有限字长表示,当超出最值(有限字长)时,需要截断(溢出,求模)操作。不同字长的整型具有不同的值域,混合运算时,需要类型提升和转换。1整形最值在<limit.h>中有整型的...

C++|漫谈STL细节及内部原理(c++ std stl)

1988年,AlexanderStepanov开始进入惠普的PaloAlto实验室工作,在随后的4年中,他从事的是有关磁盘驱动器方面的工作。直到1992年,由于参加并主持了实验室主任BillWo...

C++11新特性总结 (二)(c++11新特性 pdf)

1.范围for语句C++11引入了一种更为简单的for语句,这种for语句可以很方便的遍历容器或其他序列的所有元素vector<int>vec={1,2,3,4,5,6};f...

C++ STL 漫谈(c++中的stl到底指的什么)

标准模板库(StandardTemplateLibrary,STL)是惠普实验室开发的一个函数库和类库。它是由AlexanderStepanov、MengLee和DavidRMusser在...

C++学习教程_C++语言随到随学_不耽误上班_0基础

C++学习教程0基础学C++也可以,空闲时间学习,不耽误上班.2019年C语言新课程已经上线,随到随学,互动性强,效果好!带你征服C++语言,让所有学过和没有学过C++语言的人,或是正准备学习C++语...

C++遍历vector元素的四种方式(c++ 遍历vector)

vector是相同类型对象的集合,集合中的每个对象有个对应的索引。vector常被称为容器(container)。C++中遍历vector的所有元素是相当常用的操作,这里介绍四种方式。1、通过下标访问...

一起学习c++11——c++11中的新增的容器

c++11新增的容器1:array当时的初衷是希望提供一个在栈上分配的,定长数组,而且可以使用stl中的模板算法。array的用法如下:#include<string>#includ...

C++编程实战基础篇:一维数组应用之投票统计

题目描述班上有N个同学,有五位候选人“A,B,C,D,E”,请所有的同学投票并选举出班长,现在请你编写程序来他们计算候选人的得票总数,每位同学投票将以数字的形式投票“12345”分别代表五位候选人,...

C++20 新特性(6):new表达式也支持数组大小推导

new表达式也支持数组大小推导在C++17标准中,在定义并初始化静态数组时,是可以忽略数组大小,然后通过初始化数据来推导数组的大小。但使用new来定义并初始化动态数组时,并不支持这种自动推导数组大...

C++ 结构体(struct)最全详解(c++结构体用法)

一、定义与声明1.先定义结构体类型再单独进行变量定义structStudent{intCode;charName[20];charSex;intA...

自学 C++ 第 6 课 二维数组找最值

键盘输入一个m×n的二维数组,通过C++编程找出元素中的最大值,并输出其所在的位置坐标。例如,输入一个4×5的二维数组,数组元素分别为{{556623749},{578964563},...

从缺陷中学习C/C++:聊聊 C++ 中常见的内存问题

在写C/C++程序时,一提到内存,大多数人会想到内存泄露。内存泄露是一个令人头疼的问题,尤其在开发大的软件系统时。一个经典的现象是,系统运行了10天、1个月都好好的,忽然有一天宕机了:OOM(Out...

C++开发者都应该使用的十个C++11特性(上)

在C++11新标准中,语言本身和标准库都增加了很多新内容,本文只涉及了一些皮毛。不过我相信这些新特性当中有一些,应该成为所有C++开发者的常规装备。你也许看到过许多类似介绍各种C++11特性的文章。下...

深度解读C/C++指针与数组(c++指针和数组的区别)

指针和数组是密切相关的。事实上,指针和数组在很多情况下是可以互换的。例如,一个指向数组开头的指针,可以通过使用指针的算术运算或数组索引来访问数组。今天我们就来聊一聊数组和指针千丝万缕的关系;一维数组与...