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

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

1 自定义事件

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

html:vuex

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

js:bash

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;
		}
	}
});
复制代码

效果: app

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

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

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

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

html:spa

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

js:.net

...
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【推荐】 子组件到父组件
中央事件总线【推荐】 组件间任意通讯
父子链 组件间任意通讯
子组件索引 父组件到子组件