查看原文
其他

Vue.js 组件通信精髓归纳

(给IT平头哥联盟加星标,提升前端技能)

作者:张炳

https://segmentfault.com/a/1190000018241972

组件的分类

常规页面组件

vue-router 产生的每个页面,它本质上也是一个组件( .vue),主要承载当前页面的 HTML 结构,会包含数据获取、数据整理、数据可视化等常规业务。

功能性抽象组件

不包含业务,独立、具体功能的基础组件,比如日期选择器、弹窗警告等。这类组件作为项目的基础控件,会被大量使用,因此组件的 API 进行过高强度的抽象,可以通过不同配置实现不同的功能。

业务组件

它不像第二类独立组件只包含某个功能,而是在业务中被多个页面复用的,它与独立组件的区别是,业务组件只在当前项目中会用到,不具有通用性,而且会包含一些业务,比如数据请求;而独立组件不含业务,在任何项目中都可以使用,功能单一,比如一个具有数据校验功能的输入框。

组件的关系

父子组件

父子关系即是组件 A 在它的模板中使用了组件 B,那么组件 A 就是父组件,组件 B 就是子组件。


  1. // 注册一个子组件



  2. Vue.component('child', {



  3. data: function(){



  4. return {



  5. text: '我是father的子组件!'



  6. }



  7. },



  8. template: '<span>{{ text }}</span>'



  9. })



  10. // 注册一个父组件



  11. Vue.component('father', {



  12. template: '<div><child></child></div>' // 在模板中使用了child组件



  13. })


兄弟组件

两个组件互不引用,则为兄弟组件。


  1. Vue.component('brother1', {



  2. template: '<div>我是大哥</div>'



  3. })



  4. Vue.component('brother2', {



  5. template: '<div>我是小弟</div>'



  6. })


使用组件的时候:


  1. <div id="app">



  2. <brother1></brother1>



  3. <brother2></brother2>



  4. </div>


跨级组件

就是在父子关系中,中间跨了很多个层级。

组件的构成

一个再复杂的组件,都是由三部分组成的: propeventslot,它们构成了 Vue.js 组件的 API。

属性 prop

prop 定义了这个组件有哪些可配置的属性,组件的核心功能也都是它来确定的。写通用组件时, props 最好用对象的写法,这样可以针对每个属性设置类型、默认值或自定义校验属性的值,这点在组件开发中很重要,然而很多人却忽视,直接使用 props 的数组用法,这样的组件往往是不严谨的。

插槽 slot

插槽 slot,它可以分发组件的内容。和 HTML 元素一样,我们经常需要向一个组件传递内容,像这样:


  1. <alert-box>



  2. Something bad happened.



  3. </alert-box>


可能会渲染出这样的东西:


  1. Error!Something bad happended.


幸好,Vue 自定义的 <slot> 元素让这变得非常简单:


  1. Vue.component('alert-box', {



  2. template: `



  3. <div class="demo-alert-box">



  4. <strong>Error!</strong>



  5. <slot></slot>



  6. </div>



  7. `



  8. })


如你所见,我们只要在需要的地方加入插槽就行了——就这么简单!

自定义事件 event

两种写法:

1、在组件内部自定义事件 event


  1. <template>



  2. <button @click="handleClick">



  3. <slot></slot>



  4. </button>



  5. </template>



  6. <script>



  7. export default {



  8. methods: {



  9. handleClick (event) {



  10. this.$emit('on-click', event);



  11. }



  12. }



  13. }



  14. </script>


通过 $emit,就可以触发自定义的事件 on-click ,在父级通过 @on-click 来监听:


  1. <i-button @on-click="handleClick"></i-button>


2、用事件修饰符 .native 直接在父级声明

所以上面的示例也可以这样写:


  1. <i-button @click.native="handleClick"></i-button>


如果不写 .native 修饰符,那上面的 @click 就是自定义事件 click,而非原生事件 click,但我们在组件内只触发了 on-click 事件,而不是 click,所以直接写 @click会监听不到。

组件的通信

ref和$parent和$children

Vue.js 内置的通信手段一般有两种:

  • ref:给元素或组件注册引用信息;

  • $parent / $children:访问父 / 子实例。

ref 来访问组件(部分代码省略):


  1. // component-a



  2. export default {



  3. data () {



  4. return {



  5. title: 'Vue.js'



  6. }



  7. },



  8. methods: {



  9. sayHello () {



  10. window.alert('Hello');



  11. }



  12. }



  13. }



  1. <template>



  2. <component-a ref="comA"></component-a>



  3. </template>



  4. <script>



  5. export default {



  6. mounted () {



  7. const comA = this.$refs.comA;



  8. console.log(comA.title); // Vue.js



  9. comA.sayHello(); // 弹窗



  10. }



  11. }



  12. </script>


$parent$children 类似,也是基于当前上下文访问父组件或全部子组件的。

这两种方法的弊端是,无法在跨级或兄弟间通信,比如下面的结构:


  1. // parent.vue



  2. <component-a></component-a>



  3. <component-b></component-b>



  4. <component-b></component-b>


我们想在 component-a 中,访问到引用它的页面中(这里就是 parent.vue)的两个 component-b 组件,那这种情况下,是暂时无法实现的,后面会讲解到方法。

provide / inject

一种无依赖的组件通信方法:Vue.js 内置的 provide / inject 接口。

provide / inject 是 Vue.js 2.2.0 版本后新增的 API,在文档中这样介绍 :

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。provideinject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。

假设有两个组件: A.vueB.vue,B 是 A 的子组件:


  1. // A.vue



  2. export default {



  3. provide: {



  4. name: 'Aresn'



  5. }



  6. }




  7. // B.vue



  8. export default {



  9. inject: ['name'],



  10. mounted () {



  11. console.log(this.name); // Aresn



  12. }



  13. }


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

只要一个组件使用了 provide 向下提供数据,那其下所有的子组件都可以通过 inject 来注入,不管中间隔了多少代,而且可以注入多个来自不同父级提供的数据。需要注意的是,一旦注入了某个数据,那这个组件中就不能再声明 这个数据了,因为它已经被父级占有。

provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。然后有两种场景它不能很好的解决:

  • 父组件向子组件(支持跨级)传递数据;

  • 子组件向父组件(支持跨级)传递数据。

这种父子(含跨级)传递数据的通信方式,Vue.js 并没有提供原生的 API 来支持,下面介绍一种在父子组件间通信的方法 dispatchbroadcast

$attrs和$listeners

如果父组件 A 下面有子组件 B,组件 B 下面有组件 C,这时如果组件 A 想传递数据给组件C怎么办呢? Vue 2.4 开始提供了 $attrs$listeners 来解决这个问题,能够让组件 A 之间传递消息给组件 C。


  1. Vue.component('C',{



  2. template:`



  3. <div>



  4. <input type="text" v-model="$attrs.messagec" @input="passCData($attrs.messagec)"> </div>



  5. `,




  6. methods:{



  7. passCData(val){



  8. //触发父组件A中的事件



  9. this.$emit('getCData',val)



  10. }



  11. }



  12. })




  13. Vue.component('B',{



  14. data(){



  15. return {



  16. mymessage:this.message



  17. }



  18. },



  19. template:`



  20. <div>



  21. <input type="text" v-model="mymessage" @input="passData(mymessage)">



  22. <!-- C组件中能直接触发getCData的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性 -->



  23. <!-- 通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的) -->



  24. <C v-bind="$attrs" v-on="$listeners"></C>



  25. </div>



  26. `,



  27. props:['message'],//得到父组件传递过来的数据



  28. methods:{



  29. passData(val){



  30. //触发父组件中的事件



  31. this.$emit('getChildData',val)



  32. }



  33. }



  34. })



  35. Vue.component('A',{



  36. template:`



  37. <div>



  38. <p>this is parent compoent!</p>



  39. <B :messagec="messagec" :message="message" v-on:getCData="getCData" v-on:getChildData="getChildData(message)"></B>



  40. </div>



  41. `,



  42. data(){



  43. return {



  44. message:'hello',



  45. messagec:'hello c' //传递给c组件的数据



  46. }



  47. },



  48. methods:{



  49. getChildData(val){



  50. console.log('这是来自B组件的数据')



  51. },



  52. //执行C子组件触发的事件



  53. getCData(val){



  54. console.log("这是来自C组件的数据:"+val)



  55. }



  56. }



  57. })



  58. var app=new Vue({



  59. el:'#app',



  60. template:`



  61. <div>



  62. <A></A>



  63. </div>



  64. `



  65. })


派发与广播——自行实现 dispatch 和 broadcast 方法

要实现的 dispatchbroadcast 方法,将具有以下功能:在子组件调用 dispatch 方法,向上级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该上级组件已预先通过 $on 监听了这个事件;相反,在父组件调用 broadcast 方法,向下级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该下级组件已预先通过 $on 监听了这个事件。


  1. // 部分代码省略



  2. import Emitter from '../mixins/emitter.js'




  3. export default {



  4. mixins: [ Emitter ],



  5. methods: {



  6. handleDispatch () {



  7. this.dispatch(); // ①



  8. },



  9. handleBroadcast () {



  10. this.broadcast(); // ②



  11. }



  12. }



  13. }



  1. //emitter.js 的代码:



  2. function broadcast(componentName, eventName, params) {



  3. this.$children.forEach(child => {



  4. const name = child.$options.name;




  5. if (name === componentName) {



  6. child.$emit.apply(child, [eventName].concat(params));



  7. } else {



  8. broadcast.apply(child, [componentName, eventName].concat([params]));



  9. }



  10. });



  11. }



  12. export default {



  13. methods: {



  14. dispatch(componentName, eventName, params) {



  15. let parent = this.$parent || this.$root;



  16. let name = parent.$options.name;




  17. while (parent && (!name || name !== componentName)) {



  18. parent = parent.$parent;




  19. if (parent) {



  20. name = parent.$options.name;



  21. }



  22. }



  23. if (parent) {



  24. parent.$emit.apply(parent, [eventName].concat(params));



  25. }



  26. },



  27. broadcast(componentName, eventName, params) {



  28. broadcast.call(this, componentName, eventName, params);



  29. }



  30. }



  31. };


因为是用作 mixins 导入,所以在 methods 里定义的 dispatchbroadcast 方法会被混合到组件里,自然就可以用 this.dispatchthis.broadcast 来使用。

这两个方法都接收了三个参数,第一个是组件的 name 值,用于向上或向下递归遍历来寻找对应的组件,第二个和第三个就是上文分析的自定义事件名称和要传递的数据。

可以看到,在 dispatch 里,通过 while 语句,不断向上遍历更新当前组件(即上下文为当前调用该方法的组件)的父组件实例(变量 parent 即为父组件实例),直到匹配到定义的 componentName 与某个上级组件的 name 选项一致时,结束循环,并在找到的组件实例上,调用 $emit 方法来触发自定义事件 eventName。broadcast 方法与之类似,只不过是向下遍历寻找。

来看一下具体的使用方法。有 A.vueB.vue 两个组件,其中 B 是 A 的子组件,中间可能跨多级,在 A 中向 B 通信:


  1. <!-- A.vue -->



  2. <template>



  3. <button @click="handleClick">触发事件</button>



  4. </template>



  5. <script>



  6. import Emitter from '../mixins/emitter.js';




  7. export default {



  8. name: 'componentA',



  9. mixins: [ Emitter ],



  10. methods: {



  11. handleClick () {



  12. this.broadcast('componentB', 'on-message', 'Hello Vue.js');



  13. }



  14. }



  15. }



  16. </script>



  1. // B.vue



  2. export default {



  3. name: 'componentB',



  4. created () {



  5. this.$on('on-message', this.showMessage);



  6. },



  7. methods: {



  8. showMessage (text) {



  9. window.alert(text);



  10. }



  11. }



  12. }


同理,如果是 B 向 A 通信,在 B 中调用 dispatch 方法,在 A 中使用 $on 监听事件即可。

以上就是自行实现的 dispatchbroadcast 方法。

找到任意组件实例——findComponents 系列方法

它适用于以下场景:

  1. 由一个组件,向上找到最近的指定组件;

  2. 由一个组件,向上找到所有的指定组件;

  3. 由一个组件,向下找到最近的指定组件;

  4. 由一个组件,向下找到所有指定的组件;

  5. 由一个组件,找到指定组件的兄弟组件。

5 个不同的场景,对应 5 个不同的函数,实现原理也大同小异。

1、向上找到最近的指定组件——findComponentUpward


  1. // 由一个组件,向上找到最近的指定组件



  2. function findComponentUpward (context, componentName) {



  3. let parent = context.$parent;



  4. let name = parent.$options.name;




  5. while (parent && (!name || [componentName].indexOf(name) < 0)) {



  6. parent = parent.$parent;



  7. if (parent) name = parent.$options.name;



  8. }



  9. return parent;



  10. }



  11. export { findComponentUpward };


比如下面的示例,有组件 A 和组件 B,A 是 B 的父组件,在 B 中获取和调用 A 中的数据和方法:


  1. <!-- component-a.vue -->



  2. <template>



  3. <div>



  4. 组件 A



  5. <component-b></component-b>



  6. </div>



  7. </template>



  8. <script>



  9. import componentB from './component-b.vue';




  10. export default {



  11. name: 'componentA',



  12. components: { componentB },



  13. data () {



  14. return {



  15. name: 'Aresn'



  16. }



  17. },



  18. methods: {



  19. sayHello () {



  20. console.log('Hello, Vue.js');



  21. }



  22. }



  23. }



  24. </script>



  1. <!-- component-b.vue -->



  2. <template>



  3. <div>



  4. 组件 B



  5. </div>



  6. </template>



  7. <script>



  8. import { findComponentUpward } from '../utils/assist.js';




  9. export default {



  10. name: 'componentB',



  11. mounted () {



  12. const comA = findComponentUpward(this, 'componentA');




  13. if (comA) {



  14. console.log(comA.name); // Aresn



  15. comA.sayHello(); // Hello, Vue.js



  16. }



  17. }



  18. }



  19. </script>


2、向上找到所有的指定组件——findComponentsUpward


  1. // 由一个组件,向上找到所有的指定组件



  2. function findComponentsUpward (context, componentName) {



  3. let parents = [];



  4. const parent = context.$parent;




  5. if (parent) {



  6. if (parent.$options.name === componentName) parents.push(parent);



  7. return parents.concat(findComponentsUpward(parent, componentName));



  8. } else {



  9. return [];



  10. }



  11. }



  12. export { findComponentsUpward };


3、向下找到最近的指定组件——findComponentDownward


  1. // 由一个组件,向下找到最近的指定组件



  2. function findComponentDownward (context, componentName) {



  3. const childrens = context.$children;



  4. let children = null;




  5. if (childrens.length) {



  6. for (const child of childrens) {



  7. const name = child.$options.name;




  8. if (name === componentName) {



  9. children = child;



  10. break;



  11. } else {



  12. children = findComponentDownward(child, componentName);



  13. if (children) break;



  14. }



  15. }



  16. }



  17. return children;



  18. }



  19. export { findComponentDownward };


4、向下找到所有指定的组件——findComponentsDownward


  1. // 由一个组件,向下找到所有指定的组件



  2. function findComponentsDownward (context, componentName) {



  3. return context.$children.reduce((components, child) => {



  4. if (child.$options.name === componentName) components.push(child);



  5. const foundChilds = findComponentsDownward(child, componentName);



  6. return components.concat(foundChilds);



  7. }, []);



  8. }



  9. export { findComponentsDownward };


5、找到指定组件的兄弟组件——findBrothersComponents


  1. // 由一个组件,找到指定组件的兄弟组件



  2. function findBrothersComponents (context, componentName, exceptMe = true) {



  3. let res = context.$parent.$children.filter(item => {



  4. return item.$options.name === componentName;



  5. });



  6. let index = res.findIndex(item => item._uid === context._uid);



  7. if (exceptMe) res.splice(index, 1);



  8. return res;



  9. }



  10. export { findBrothersComponents };


相比其它 4 个函数, findBrothersComponents 多了一个参数 exceptMe,是否把本身除外,默认是 true。寻找兄弟组件的方法,是先获取 context.$parent.$children,也就是父组件的全部子组件,这里面当前包含了本身,所有也会有第三个参数 exceptMe。Vue.js 在渲染组件时,都会给每个组件加一个内置的属性 _uid,这个 _uid 是不会重复的,借此我们可以从一系列兄弟组件中把自己排除掉。

Event Bus

有时候两个组件之间需要进行通信,但是它们彼此不是父子组件的关系。在一些简单场景,你可以使用一个空的 Vue 实例作为一个事件总线中心(central event bus):


  1. //中央事件总线



  2. var bus=new Vue();




  3. var app=new Vue({



  4. el:'#app',



  5. template:`



  6. <div>



  7. <brother1></brother1>



  8. <brother2></brother2>



  9. </div>



  10. `



  11. })




  12. // 在组件 brother1 的 methods 方法中触发事件



  13. bus.$emit('say-hello', 'world')




  14. // 在组件 brother2 的 created 钩子函数中监听事件



  15. bus.$on('say-hello', function (arg) {



  16. console.log('hello ' + arg); // hello world



  17. })


vuex处理组件之间的数据交互

如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候才有上面这一些方法可能不利于项目的维护,vuex 的做法就是将这一些公共的数据抽离出来,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。 详情可参考:https://vuex.vuejs.org/zh-cn/。

参考

  • vue组件之间8种组件通信方式总结(https://blog.csdn.net/zhoulu001/article/details/79548350)

  • https://github.com/iview/iview/blob/2.0/src/mixins/emitter.js

  • https://github.com/iview/iview/blob/2.0/src/utils/assist.js

- end -


用心分享 一起成长 做有温度的攻城狮

每天记得对自己说:你是最棒的!


好文阅读:


有强迫症患者该如何用vscode开发Vue应用?

是时候了解一下如何把SVG Sprites应用到项目中了

开发一个静态 HTML 页,我要价 18000 美元,有错吗?

趋势:TypeScript - 一种思维方式

即日起 TypeScript —— 面向编辑器编程

拆解 JavaScript 中的异步模式

JavaScript 究竟是如何工作的?(第一发)

那么JavaScript 究竟是如何工作的?(第二发)

前端组件设计原则



都看到这里了,给个“在看”再走呗~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存