vue组件通讯--注意事项及经验总结

转自vue组件通讯--注意事项及经验总结

说说在 Vue.js 中如何实现组件间通讯(高级篇)

写在前面

组件间的通讯是是实际开发中很是经常使用的一环,如何使用对项目总体设计、开发、规范都有很实际的的做用,我在项目开发中对此深有体会,总结下vue组件间通讯的几种方式,讨论下各自的使用场景html

文章对相关场景预览vue

  • 父->子组件间的数据传递
  • 子->父组件间的数据传递
  • 兄弟组件间的数据传递
  • 组件深层嵌套,祖先组件与子组件间的数据传递

文章相关技术预览 prop、emit、bus、vuex、路由URL、provide/inject、children/parent、attrs/inheritAttrsvuex

注:如下介绍与代码环境:vue2.0+、vue-cli2vue-cli

父->子组件间的数据传递

父子组件的通讯是开发是最经常使用的也是最重要的,大家必定知道父子通讯是用prop传递数据的,像这样:api

//父组件,传递数据
<editor :inputIndex="data" :inputName="王文健"></editor>
复制代码

//子组件,接受数据,定义传递数据的类型type与默认值default
    props: {
        inputIndex: {
            type: Object, 
            default: function(){
                return {}
            }
        },
        inputName: {
            type: String,
            default: ''
        },
复制代码
复制代码

注意项数组

父组件传递数据时相似在标签中写了一个属性,若是是传递的数据是data中的天然是要在传递属性前加v-bind:,若是传递的是一个已知的固定值呢session

  • 字符串是静态的可直接传入无需在属性前加v-bind
  • 数字,布尔,对象,数组,由于这些是js表达式而不是字符串,因此即便这些传递的是静态的也须要加v-bind,把数据放到data中引用,

若是prop传到子组件中的数据是一个对象的话,要注意传递的是一个对象引用,虽然父子组件看似是分离的但最后都是在同一对象下app

  • 若是prop传到子组件的值只是做为初始值使用,且在父组件中不会变化赋值到data中使用dom

  • 若是传到子组件的prop的数据在父组件会被改变的,放到计算属性中监听变化使用。由于若是传递的是个对象的话,只改变下面的某个属性子组件中是不会响应式更新的,若是子组件须要在数据变化时响应式更新那只能放到computed中或者用watch深拷贝deep:true才能监听到变化ide

  • 固然若是你又须要在子组件中经过prop传递数据的变化作些操做,那么写在computed中会报警告,由于计算属性中不推荐有任何数据的改变,最好只进行计算。若是你非要进行数据的操做那么能够把监听写在watch(注意deep深拷贝)或者使用computed的getset以下图:![计算属性.png](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="www.w3.org/2000/svg" version="1.1" width="994" height="748"></svg>)

  • 但问题又来了,若是你传进来的是个对象,同时你又须要在子组件中操做传进来的这个数据,那么在父组件中的这个数据也会改变由于你传递的只是个引用, 即便你把prop的数据复制到data中也是同样的,不管如何赋值都是引用的赋值,你只能对对象作深拷贝建立一个副本才能继续操做,你能够用JSON的方法先转化字符串在转成对象更方便一点,

  • 因此在父子传递数据时要先考虑好数据要如何使用,不然你会遇到不少问题或子组件中修改了父组件中的数据,这是很隐蔽而且很危险的

子->父组件间的数据传递

在vue中子向父传递数据通常用**$emit**自定义事件,在父组件中监听这个事件并在回调中写相关逻辑

// 父组件监听子组件定义的事件
 <editor :inputIndex="index" @editorEmit='editorEmit'></editor>
复制代码

// 子组件须要返回数据时执行,并能够传递数据
this.$emit('editorEmit', data)
复制代码
复制代码

那么问题来了,我是否是真的有必要去向父组件返回这个数据,用自定义事件能够在当子组件想传递数据或向子组件传递的数据有变化须要从新传递时执行,那么另一种场景,父组件须要子组件的一个数据但子组件并不知道或者说没有能力在父组件想要的时候给父组件,那么这个时候就要用到组件的一个选项ref

<editor ref="editor" @editorEmit='editorEmit'></editor>

  • 父组件在标签中定义ref属性,在js中直接调用this.$refs.editor就是调用整个子组件,子组件的全部内容都能经过ref去调用,固然咱们并不推荐由于这会使数据看起来很是混乱,
  • 因此咱们能够在子组件中定义一种专供父组件调用的函数,,好比咱们在这个函数中返回子组件data中某个数据,**当父组件想要获取这个数据就直接主动调用ref执行这个函数获取这个数据,**这样能适应很大一部分场景,逻辑也更清晰一点
  • 另外,父向子传递数据也能够用ref,有次须要在一个父组件中大量调用同一个子组件,而每次调用传递的prop数据都不一样,而且传递数据会根据以后操做变化,这样我须要在data中定义大量相关数据并改变它,我能够直接用ref调用子组件函数直接把数据以参数的形式传给子组件,逻辑一会儿清晰了
  • 若是调用基础组件能够在父组件中调用ref执行基础组件中暴露的各类功能接口,好比显示,消失等

兄弟组件间的数据传递

vue中兄弟组件间的通讯是很不方便的,或者说不支持的,那么父子组件中都有什么通讯方式呢

  • 路由URL参数

  • 在传统开发时咱们经常把须要跨页面传递的数据放到url后面,跳转到另外页面时直接获取url字符串获取想要的参数便可,在vue跨组件时同样能够这么作, // router index.js 动态路由 { path:'/params/:Id', component:Params, name:Params } 复制代码

    // 跳转路由 跳转路由 复制代码

  • 在跳转后的组件中用$route.params.id去获取到这个id参数为12,但这种只适合传递比较小的数据,数字之类的

  • Bus通讯

在组件以外定义一个bus.js做为组件间通讯的桥梁,适用于比较小型不须要vuex又须要兄弟组件通讯的

bus.js中添加以下

import Vue from 'vue'export default new Vue
复制代码
复制代码

组件中调用bus.js经过自定义事件传递数据

import Bus from './bus.js'export default { 
      methods: {
         bus () {
            Bus.$emit('msg', '我要传给兄弟组件们')
         }
      }
  }
复制代码
复制代码

兄弟组件中监听事件接受数据

import Bus from './bus.js'export default {
        mounted() {
           Bus.$on('msg', (e) => {
             console.log(e)
           })
         }
       }
复制代码
复制代码

注:以上两种使用场景并不高因此只是简略提一下,这两点都是好久之前写过,以上例子网上直接搜集而来若有错误,指正

  • Vuex集中状态管理 vuex是vue的集中状态管理工具,对于大型应用统一集中管理数据,很方便,在此对vuex的用法并不过多介绍只是提一下使用过程当中遇到的问题

规范:对于多人开发的大型应用规范的制定是相当重要的,对于全部人都会接触到的vuex对其修改数据调用数据都应有一个明确严格的使用规范

  1. vuex分模块:项目不一样模块间维护各自的vuex数据
  2. 限制调用:只容许action操做数据,getters获取数据,使用mapGetters,mapActions辅助函数调用数据![vuex.png](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="www.w3.org/2000/svg" version="1.1" width="1136" height="1280"></svg>)

对于vuex的使用场景也有一些争论,有人认为正常组件之间就是要用父子组件传值的方式,即便子组件须要使vuex中的数据也应该由父组件获取再传到子组件中,但有的时候组件间嵌套很深,只容许父组件获取数据并非一个方便的方法,因此对于祖先元组件与子组件传值又有了新问题,vue官网也有一些方法解决,以下

祖先组件与子组件间的数据传递

provide/inject 除了正常的父子组件传值外,vue也提供了provide/inject

这对选项须要一块儿使用,以容许一个祖先组件向其全部子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效

官网实例

// 父级组件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
}
复制代码
复制代码
  • provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。

  • 一个字符串数组,或 一个对象,对象的 key 是本地的绑定名,value 是:

  • 在可用的注入内容中搜索用的 key (字符串或 Symbol),或 一个对象,该对象的:

  • from 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)

  • default 属性是降级状况下使用的 value

提示:provide 和 inject 绑定并非可响应的。这是刻意为之的。然而,若是你传入了一个可监听的对象,那么其对象的属性仍是可响应的。 具体细节移步vue相关介绍cn.vuejs.org/v2/api/#pro…

provide/inject还未在项目中应用过,后面会作尝试


补充 $attrs/inheritAttrs

经小伙伴们提醒补充$attrs的使用

场景:祖先组件与子组件传值

  • 若是是props的话,就必须在子组件与祖先组件之间每一个组件都要prop接受这个数据,再传到下一层子组件,这就很麻烦,耦合深程序臃肿
  • 若是用vuex确实显得有点小题大作了,因此用$attrs直接去获取祖先数据也不错

包含了父做用域中不做为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含全部父做用域的绑定 (class 和 style 除外),而且能够经过 v-bind="$attrs" 传入内部组件——在建立高级别的组件时很是有用。

以上是官网对$attrs的解释,我刚看我也是一脸懵逼,回去试了一下其实并不难,并且比较适用组件深层嵌套场景下,祖先组件向子组件传值的问题

意思就是父组件传向子组件传的,子组件不prop接受的数据都会放在attrs中,子组件直接用this.attrs获取就能够了。如过从父->孙传,就在子组件中添加v-bind='$attrs',就把父组件传来的子组件没props接收的数据所有传到孙组件,具体看如下代码

使用:

祖先组件

// 祖先组件
// 在祖先组件中直接传入output和input
<template>
  <div>
    <child1 :output='output' :input="input"></child1>
  </div>
</template>
<script>
import child1 from './child1.vue'export default {
  components: {
    child1
  },
  data () {
    return {
      input: 'jijijijjijiji',
      output: {
        name: '王文健',
        age: '18'
      }
    }
  }
</script>
复制代码
复制代码

子组件

<template>
  <div
    <h1>{{input}}</h1>
    <child2 :child="child" v-bind='$attrs'></child2>
  </div>
</template>
<script>
import child2 from './child2.vue'export default {
  components: {
    child2
  },
  props: {
    input: [String]
  },
  data () {
    return {
      child: 'child1child1child1child1s'
    }
  },
// 默认为true,若是传入的属性子组件没有prop接受,就会以字符串的形式出现为标签属性
// 设为false,在dom中就看不到这些属性,试一下就知道了
  inheritAttrs: false,
  created () {
    // 在子组件中打印的$attrs就是父组件传入的值,刨去style,class,和子组件中已props的属性
    console.log(this.$attrs)  // 打印output
  }
}
</script>

复制代码
复制代码

孙组件

<template>
  <div>
    {{$attrs.output.name}}
  </div>
</template>
<script>
export default {
  created () {
    // 打印output和child
    console.log(this.$attrs)
  }
}
</script>

复制代码
复制代码

看起来仍是挺好用的,还没在具体项目中用过,相信不久会用到的,若是还有什么问题欢迎留言

children/parent

固然你能够直接用children/parent获取当前组件的子组件实例或父组件实例(若是有的话),也能对其作些操做,不过并不推荐这么作

你还能够放到localStorage,sessionStorage,cooikes之类的存在本地固然也能作到组件间的通讯


以前说过,可使用 props 将数据从父组件传递给子组件。其实还有其它种的通讯方式,下面咱们一一娓娓道来。

1 自定义事件

经过自定义事件,咱们能够把数据从子组件传输回父组件。子组件经过 $emit() 来触发事件,而父组件经过 $on() 来监听事件,这是典型的观察者模式。

html:

<div id="app">
    <p>总数:{{total}}</p>
    <deniro-component @increase="setTotal"
                      @reduce="setTotal"
    ></deniro-component>
</div>
复制代码
复制代码

js:

Vue.component('deniro-component', {
	template: '\
	<div>\
	<button @click="increase">+1</button>\
	<button @click="reduce">-1</button>\
	</div>',
	data: function () {
		return {
			counter: 0
		}
	},
	methods: {
		increase: function () {
			this.counter++;
			this.$emit('increase', this.counter);
		},
		reduce: function () {
			this.counter--;
			this.$emit('reduce', this.counter);
		}
	}
});

var app = new Vue({
	el: '#app',
	data: {
		total: 0
	},
	methods: {
		setTotal: function (total) {
			this.total = total;
		}
	}
});
复制代码
复制代码

效果:

示例中有两个按钮,分别实现加 1 与减 1 操做。点击按钮后,执行组件中定义的 increase 或 reduce 方法,在方法内部,使用 $emit 把值传递回父组件。 $emit 方法的第一个参数是使用组件时定义的事件名,示例中是 @increase@reduce

<deniro-component @increase="setTotal"
				  @reduce="setTotal"
></deniro-component>
复制代码
复制代码

这两个事件又绑定了 setTotal 方法,该方法修改了 total 值。 $emit 方法的其它参数是须要回传给父组件的参数。

也可使用 v-on.native 来监听原生事件,好比这里监听组件的点击事件:

html:

<div id="app">
    ...
    <deniro-component ...
                      @click.native="click"
    ></deniro-component>
</div>
复制代码
复制代码

js:

...
var app = new Vue({
	el: '#app',
	data: {
		total: 0
	},
	methods: {
		...
		click: function () {
			console.log("原生点击事件");
		}
	}
});
复制代码
复制代码

这样,点击按钮后,就能够捕获原生的点击事件啦O(∩_∩)O~

**注意:**这里监听的是这个组件根元素的原生点击事件。

2 v-model 方式

也可使用 v-model 方式来直接绑定父组件变量,把数据从子组件传回父组件。

html:

<div id="app2">
    <p>总数:{{total}}</p>
    <deniro-component2 v-model="total"
    ></deniro-component2>
</div>
复制代码
复制代码

js:

Vue.component('deniro-component2', {
	template: '\
	<div>\
	<button @click="click">+1</button>\
	</div>',
	data: function () {
		return {
			counter: 0
		}
	},
	methods: {
		click: function () {
			this.counter++;
			this.$emit('input', this.counter);
		}
	}
});

var app2 = new Vue({
	el: '#app2',
	data: {
		total: 0
	}
});
复制代码
复制代码

效果:

咱们使用 v-model="total" 直接绑定变量 total。接着在子组件中,在 $emit 方法传入事件名 input,这样 Vue.js 就会自动找到 `v-model 绑定的变量啦O(∩_∩)O~

咱们也可使用自定义事件来实现上述示例——

html:

<div id="app3">
    <p>总数:{{total}}</p>
    <deniro-component3 @input="setTotal"
    ></deniro-component3>
</div>

复制代码
复制代码

js:

Vue.component('deniro-component3', {
	template: '\
	<div>\
	<button @click="click">+1</button>\
	</div>',
	data: function () {
		return {
			counter: 0
		}
	},
	methods: {
		click: function () {
			this.counter++;
			this.$emit('input', this.counter);
		}
	}
});

var app3 = new Vue({
	el: '#app3',
	data: {
		total: 0
	},
	methods: {
		setTotal: function (total) {
			this.total = total;
		}
	}
});
复制代码
复制代码

效果与上例相同。

咱们还能够在自定义的表单输入组件中利用 v-model,实现数据双向绑定:

html:

<div id="app4">
    <p>总数:{{total}}</p>
    <deniro-component4 v-model="total"
    ></deniro-component4>
    <button @click="increase">+1</button>
</div>
复制代码
复制代码

js:

Vue.component('deniro-component4', {
	props: ['value'],
	template: '<input :value="value" @input="update">+1</input>',
	data: function () {
		return {
			counter: 0
		}
	},
	methods: {
		update: function (event) {
			this.$emit('input', event.target.value);
		}
	}
});

var app4 = new Vue({
	el: '#app4',
	data: {
		total: 0
	},
	methods: {
		increase: function () {
			this.total++;
		}
	}
});
复制代码
复制代码

效果:

这里咱们首先利用 v-model,在自定义组件中绑定了 total 变量。而后在组件内部,定义了 props 为 ['value']注意这里必须为 value,才能接收绑定的 total 变量。接着在组件模板中把接收到的 value 值(即 total 变量值),做为 <input> 元素的初始值,并绑定 input 事件。下一步,在 input 事件中,经过 this.$emit('input', event.target.value) 把 total 值传回父组件的 <button @click="increase">+1</button>。最后在 increase 方法中,递增 total 值。

这个示例,咱们综合使用了 props 、v-model和自定义事件,实现了数据的双向绑定。

总的来讲,一个具备双向绑定的 v-model 组件具备如下特征:

  1. 使用 props 接收父组件的 value。
  2. 子组件中拥有能够更新 value 的 HTML 元素,当更新 value 时,触发 input 事件。事件内部使用 $emit 将新的 value 值回传给父组件。

3 非父子组件

非父子组件指的是兄弟组件或者跨多级组件。

3.1 中央事件总线

咱们能够建立一个空的 Vue 实例做为中央事件总线,实现非父子组件之间的通讯。

html:

<div id="app5">
    <p>监听子组件消息:{{message}}</p>
    <deniro-component5></deniro-component5>
</div>
复制代码
复制代码

js:

var bus = new Vue();
Vue.component('deniro-component5', {
	template: '<button @click="sendMessage">发送消息</button>',
	methods: {
		sendMessage: function () {
			bus.$emit('on-message', '来自于 deniro-component5 的消息');
		}
	}
});
var app5 = new Vue({
	el: "#app5",
	data: {
		message: ''
	},
	mounted: function () {
		var that = this;

		bus.$on('on-message', function (message) {
			that.message = message;
		})
	}
});
复制代码
复制代码

注意: 由于 bus.$on() 中的函数,this 指向的是自己,因此咱们必须在外层定义一个 that,让它引用 mounted 对象。

效果:

首先建立了一个空的 Vue 实例做为中央事件总线。而后在定义的子组件绑定的 click 事件中,经过 bus.$emit() 发送消息。接着在初始化 app 实例的 mounted 函数时,使用 bus.$on() 方法监听消息。

这种方式能够实现组件间任意通讯。咱们还能够扩展 bus 实例,为它添加 data、methods、computed 等属性,这些都是公共属性,能够共用。因此在此能够放置须要共享的信息,好比用户登录昵称等。使用时只须要初始化一次 bus 便可,因此在单页面富客户端中应用普遍。

若是项目较大,那么可使用具备状态管理的 vuex 哦O(∩_∩)O~

3.2 父子链

子组件可使用 this.$parent 来访问父组件实例;而父组件可使用 this.$children 来访问它的全部子组件实例。这些方法能够递归向上或向下,直到根实例或者叶子实例。

html:

<div id="app6">
    <p>消息:{{message}}</p>
    <deniro-component6></deniro-component6>
</div>
复制代码
复制代码

js:

Vue.component('deniro-component6', {
	template: '<button @click="sendMessage">发送消息</button>',
	methods: {
		sendMessage: function () {
			//经过父链找到父组件,修改相应的变量
			this.$parent.message='来自于 deniro-component6 的消息';
		}
	}
});
var app6 = new Vue({
	el: "#app6",
	data: {
		message: ''
	}
});
复制代码
复制代码

效果:

**注意:**只有在万不得已的状况下,才使用父子链,实现组件间任意通讯。由于这样作,会让两个组件之间紧耦合,代码变得难理解与维护。若是只是父子组件之间的通讯,尽可能采用 props 与自定义事件 $emit 来实现。

3.3 子组件索引

若是一个组件的子组件较多且是动态渲染的场景,使用 this.$children 来遍历这些子组件较麻烦。这时就可使用 ref 来为子组件指定索引名称,方便后续查找。

html:

<div id="app7">
    <button @click="getChild">获取子组件实例</button>
    <deniro-component7 ref="child"></deniro-component7>
</div>
复制代码
复制代码

js:

Vue.component('deniro-component7', {
	template: '<div>deniro-component7</div>',
	data: function () {
		return {
			message: '登录不到两周,InSight探测器意外捕捉到火星的风声'
		}
	}
});
var app7 = new Vue({
	el: "#app7",
	methods: {
		getChild: function () {
			//使用 $refs 来访问组件实例
			console.log(this.$refs.child.message);
		}
	}
});
复制代码
复制代码

输出结果:

登录不到两周,InSight探测器意外捕捉到火星的风声

注意:$refs 只在组件渲染完成后才会被赋值,并且它是非响应式的。因此只有在万不得已的状况下才使用它。

本文示例代码


总结以下: 通讯方式通讯方向props【推荐】父组件到子组件自定义事件 $emit【推荐】子组件到父组件中央事件总线【推荐】组件间任意通讯父子链组件间任意通讯子组件索引父组件到子组件

相关文章
相关标签/搜索