在 Vue 中通常以一个 Vue 实例来表示一个应用,一个应用由若干个组件拼装而成。
没错,就像“装机”一样。当你把主板、CPU、显卡、内存、散热器、SSD、电源等摆放到机箱的各个位置后,很明显产生了一个明确的需求:我怎么让这些东西协同工作?
回到 Vue 中,处理不同组件之间的数据或状态是一件经常遇到的事。

好在 Vue的文档 足够详细。关于组件交互的部分,如果没有实际需求,我表示难以明白props以及自定义事件等使用场景是什么。

Props

props是定义在子组件中的属性,用来定义期望从父组件传下来的数据。

从实际场景着手,写一个简单的需求。

<!-- 子组件模板 -->
<template>
<p>{{helloWorld}}</p>
</template>
<!-- 父组件模板 -->
<template>
<input type="text" v-model="text">
<child></child>
</template>

当父组件中输入内容时显示到子组件中。

这时,我需要在子组件中声明一个 props 属性来接收父组件中输入的内容。

// 子组件
module.exports = {
props: {
helloWorld: String
}
};
// 父组件
module.exports = {
components: {
child: require('child')
},
data: function(){
return {
text: ''
}
}
};

还需要告诉子组件,它的 props 对应父组件中的哪个数据。

<!-- 在子组件上标记 -->
<child :hello-world="text"></child>

camelCase 格式属性用作 HTML 特性时需要转换成 kebab-case 格式。

当前需求轻松的解决了!

有没有发现 props 好像是单向的,父 -> 子?

不,并不是。只是默认是单向的。可以通过添加额外的修饰符来显示强制双向或单词绑定。

<!-- 双向绑定 -->
<child :hello-world.sync="text"></child>
<!-- 单次绑定 -->
<child :hello-world.once="text"></child>

而正像 Vue 文档中所说的,默认单向绑定是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。

举个例子,如果当前子组件设置为双向绑定,另有其他的子组件依赖父组件的helloWorld属性。这种关系下如果子组件修改了数据,势必引起其他子组件的状态改变。某些场景下我们并不希望发生这种情况。

自定义事件

使用自定义事件也可以实现父子组件之间的通信,通过事件触发的形式来传递数据。

  • 使用$on()监听事件;
  • 使用$emit()在它上面触发事件;
  • 使用$dispatch()派发事件,事件沿着父链冒泡;
  • 使用$broadcast()广播事件,事件向下传导给所有的后代。

现在变更需求,把输入框拿出来作为子组件B。

<!-- 子组件B模板 -->
<template>
<input type="text" >
</template>
<!-- 父组件模板 -->
<template>
<child-b></child-b>
<child></child>
</template>

当子组件B内容变化时,我应当通知父组件:头儿,我的工作完成了。

<template>
<input type="text" v-model="text" @change="onInput">
</template>
<script>
module.exports = {
data: function () {
return {
text: ''
}
},
methods: {
onInput: function () {
if(this.text.trim()) {
this.$dispatch('child-next', this.text);
}
}
}
};
</script>

父组件收到通知后,广播给其他需要的子组件:B已经完成XXX了,剩下的东西交给你了。

module.exports = {
components: {
child: require('child'),
'child-b': require('childB')
},
events: {
'child-next': function (text) {
this.$broadcast('child-finish', text);
}
}
};

或者为了能从父组件中直观的看出事件来源,可以使用显示声明绑定事件。

<template>
<child-b @child-next="handle"></child-b>
<child></child>
</template>
<script>
module.exports = {
components: {
child: require('child'),
'child-b': require('childB')
},
methods: {
handle: function (text) {
this.$broadcast('child-finish', text);
}
}
};
</script>

接收广播的子组件,需要添加对应的处理事件。

<template>
<p>{{helloWorld}}</p>
</template>
<script>
module.exports = {
data: function () {
return {
helloWorld: ''
}
},
events: {
'child-finish': function (text) {
this.helloWorld = text;
}
}
};
</script>

与 props 方式相比,自定义事件的方式各个组件的数据独立,不会被父或子组件轻易修改。因为我们能控制在何时进行事件派发和广播。

当然这两种方式并不冲突,可以结合使用来创造最佳实践。

Vuex

最后就是使用大杀器 Vuex 了。

不管是props还是自定义事件,如果数据要由子组件到另一个子组件中,都要进行父组件的中转。随着项目的逐步增大,数据流也会变得复杂,难以管理和发现问题。

而 Vuex 就是独立的一个数据管理层。你需要把组件的本地状态和应用状态区分开来,把应用状态交由 Vuex 来管理,方便每一个组件去交换数据更新状态。

这是一个简单的例子