在
vue
中数据流是单向的,通常父子组件通信props
或者自定义事件
,或者还有provide/inject
,甚至借助第三方数据流方案vuex
,在通常的项目中我们会高频用到哪些通信方案?
本文是笔者总结过往项目,在vue
使用到的一些数据通信方案,希望在实际项目中有些帮助和思考。
正文开始...
我们先看下在vue
中我能想到的数据通信方案
1、props
父传子
2、自定义事件@event="query"
3、.sync
修饰符
3、vuex
跨组件通信
4、Vue.observable
5、provide/inject
6、EventBus
7、refs、parent
基于以上几点,笔者用一个实际的todolist
来举证所有的通信方式
props 父组件传递子组件数据的接口通信
代码语言:javascript复制// todoList.vue
<template>
<div class="todo-list">
<h1>todo list</h1>
<Search />
<Content :dataList="dataList"/>
</div>
</template>
<script>
import Search from './Search.vue';
import Content from './Content.vue';
export default {
name: 'todo-list',
components: {Search, Content},
data() {
return {
dataList: [
{
title: 'vuejs',
subTitle: 'vuejs is crazy'
},
{
title: 'reactjs',
subTitle: 'reactjs is beautify'
}
]
}
},
methods: {}
}
</script>
父组件以Index.vue
为例,传入的子组件Content.vue
的props
就是:dataList="dataList"
在Content.vue
中我们可以看到就是通过props
上的dataList
获取父组件数据的。
<!--Content.vue-->
<template>
<div class="content">
<template v-for="(item, index) in dataList">
<h1 :key="index">{{item.title}}</h1>
<h2 :key="item.subTitle">{{item.subTitle}}</h2>
</template>
</div>
</template>
<script>
export default {
props: {
dataList: {
type: Array,
default: () => []
}
}
}
</script>
子组件数据通过父组件传递,页面数据就显示出来了
自定义事件emit通信
代码语言:javascript复制...
<div class="todo-list">
<h1>todo list</h1>
<Search @handleAdd="handleAdd"/>
<Content :dataList="dataList"/>
</div>
<script>
export default {
name: 'todo-list',
methods: {
handleAdd(params) {
this.dataList.push(params)
}
}
}
</script>
我们看到在父组件中加入了@handleAdd
自定义事件
在Search.vue
中我们引入对应逻辑
<!--Search.vue-->
<div class="search">
<a-row type="flex" justify="center" >
<a-col :span="4">
<a-input placeholder="Basic usage" v-model="value" @pressEnter="handleAdd"></a-input>
</a-col>
<a-col :span="2">
<a-button type="dashed" @click="handleAdd">添加</a-button>
</a-col>
</a-row>
</div>
代码语言:javascript复制// Search.vue
export default {
name: 'search',
data() {
return {
value: '',
odd: 0
}
},
methods: {
handleAdd() {
const {value: title} = this;
if (title === '') {
return;
}
this.odd = !this.odd;
this.$emit('handleAdd', {
title,
subTitle: `${title} is ${this.odd ? 'crazy' : 'beautify'}`
})
}
}
}
我们可以看到自定义事件
子组件中就是这么给父组件通信的
...
this.$emit('handleAdd', {
title,
subTitle: `${title} is ${this.odd ? 'crazy' : 'beautify'}`
})
.sync实现props的双向数据通信
在vue中提供了.sync
修饰符,本质上就是便捷处理props
单向数据流,因为有时候我们想直接在子组件中修改props
,但是vue
中是会警告的,如果实现props
类似的双向数据绑定,那么可以借用.sync修饰符
,这点项目里设计弹框时经常有用。
同样是上面todolist
的例子
<template>
<div class="todo-list">
<h1>todo list-sync</h1>
<Search :dataList.sync="dataList"/>
<Content :dataList="dataList"/>
</div>
</template>
<script>
import Search from './Search.vue';
import Content from './Content.vue';
export default {
name: 'todo-list',
components: {Search, Content},
data() {
return {
dataList: [
{
title: 'vuejs',
subTitle: 'vuejs is crazy'
},
{
title: 'reactjs',
subTitle: 'reactjs is beautify'
}
]
}
},
}
</script>
我们在看下Search.vue
已经通过:dataList.sync="dataList"
在props
上加了修饰符了
在Search.vue
中可以看到
...
<script>
export default {
name: 'search',
props: {
dataList: {
type: Array,
default: () => []
}
},
data() {
return {
value: '',
odd: 0
}
},
methods: {
handleAdd() {
const {value: title, dataList } = this;
if (title === '') {
return;
}
this.odd = !this.odd;
const item = {
title,
subTitle: `${title} is ${this.odd ? 'crazy' : 'beautify'}`
}
this.$emit('update:dataList', dataList.concat(item))
}
}
}
</script>
注意我们在handleAdd
方法中修改了我们是用以下这种方式去与父组件通信的,this.$emit('update:dataList', dataList.concat(item))
。
...
const {value: title, dataList } = this;
const item = {
title,
subTitle: `${title} is ${this.odd ? 'crazy' : 'beautify'}`
}
this.$emit('update:dataList', dataList.concat(item))
sync
本质也是利用自定义事件通信,上面代码就是下面的简版,我们可以利用.sync
修饰符实现props
的双向数据绑定,因此在实际项目中可以用.sync
修饰符简化业务代码,实际与下面代码等价
<Search :dataList="dataList" @update="update"/>
vuex
vuex
在具体业务中基本上都有用,我们看下vuex
是如何实现数据通信的,关于`vuex`[1]如何使用参考官方文档,这里不讲如何使用vuex,贴上关键代码
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state = {
dataList: [
{
title: 'vuejs',
subTitle: 'vuejs is crazy'
},
{
title: 'reactjs',
subTitle: 'reactjs is beautify'
}
]
};
const mutations = {
handAdd(state, payload) {
state.dataList = state.dataList.concat(payload)
}
}
export const store = new Vuex.Store({
state,
mutations
})
然后在main.js
中引入
// main.js
...
import {store} from '@/store/index';
...
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
router,
components: { App },
template: '<App/>'
})
我们看下主页面路由页面,现在变成这样了,父组件没有任何props
与自定义事件
,非常的干净。
<template>
<div class="todo-list">
<h1>todo list-vuex</h1>
<Search />
<Content/>
</div>
</template>
<script>
import Search from './Search.vue';
import Content from './Content.vue';
export default {
name: 'todo-list',
components: {Search, Content}
}
</script>
然后看下Search.vue
与Content.vue
组件
<!--Search.vue-->
<template>
<div class="search">
<a-row type="flex" justify="center" >
<a-col :span="4">
<a-input placeholder="Basic usage" v-model="value" @pressEnter="handleAdd"></a-input>
</a-col>
<a-col :span="2">
<a-button type="dashed" @click="handleAdd">添加</a-button>
</a-col>
</a-row>
</div>
</template>
<script>
export default {
name: 'search',
data() {
return {
value: '',
odd: 0
}
},
methods: {
handleAdd() {
const {value: title } = this;
if (title === '') {
return;
}
this.odd = !this.odd;
const item = {
title,
subTitle: `${title} is ${this.odd ? 'crazy' : 'beautify'}`
}
this.$store.commit('handAdd', item);
}
}
}
</script>
你会发现操作数据是用$store.commit('mutationName', data)
这个vuex
提供的同步操作去修改数据的。在Content.vue
中就是直接从store
中获取state
就行了
<template>
<div class="content">
<template v-for="(item, index) in dataList">
<h1 :key="index">{{item.title}}</h1>
<h2 :key="item.subTitle">{{item.subTitle}}</h2>
</template>
</div>
</template>
<script>
export default {
computed: {
dataList() {
return this.$store.state.dataList;
}
}
}
</script>
vuex
的思想就是数据存储的一个仓库,数据共享,本质store也是一个单例模式,所有的状态数据以及事件挂载根实例上,然后所有组件都能访问和操作,但是这么简单的功能引入一个状态管理工具
貌似有点杀鸡用牛刀了,接下来我们用官方提供的跨组件方案。
Vue.observable
vue提供一个这样的一个最小跨组件通信方案,我们具体来看下,新建一个目录todoList-obsever/store/index.js
,我们会借鉴vuex
的一些思想,具体代码如下
// store/index.js
import Vue from 'vue';
const state = {
dataList: [
{
title: 'vuejs',
subTitle: 'vuejs is crazy'
},
{
title: 'reactjs',
subTitle: 'reactjs is beautify'
}
],
commit: {
handAdd:(payload) => {
state.dataList = state.dataList.concat(payload)
},
handleDelete(index) {
state.dataList.splice(index, 1);
}
}
};
const mutations = {
commit(actionName, payload) {
if (Reflect.has(state.commit, actionName)) {
state.commit[actionName](payload)
}
},
dispatch(actionName, payload) {
mutations.commit(actionName, payload);
}
}
const store = {
state,
...mutations,
}
export default Vue.observable(store);
然后在Content.vue
中
<template>
<div class="content">
<template v-for="(item, index) in dataList">
<div :key="index" class="list">
<h1 :key="index">{{ item.title }}</h1>
<h2 :key="item.subTitle">{{ item.subTitle }}</h2>
<a-button
type="danger"
class="del"
:key="`${index}-${item.title}`"
@click="handleDelete(index)"
>删除</a-button
>
</div>
</template>
</div>
</template>
<script>
// 引入上面的store
import store from './store/index';
export default {
computed: {
dataList() {
return store.state.dataList;
}
},
methods: {
handleDelete(index) {
store.commit('handleDelete', index)
}
}
}
</script>
<style lang="scss">
.list {
.del {
position: relative;
top:-70px;
left: 160px;
}
}
</style>
在Search.vue
中
<template>
<div class="search">
<a-row type="flex" justify="center" >
<a-col :span="4">
<a-input placeholder="Basic usage" v-model="value" @pressEnter="handleAdd"></a-input>
</a-col>
<a-col :span="2">
<a-button type="dashed" @click="handleAdd">添加</a-button>
</a-col>
</a-row>
</div>
</template>
<script>
// 引入store
import store from './store/index';
export default {
name: 'search',
data() {
return {
value: '',
odd: 0
}
},
methods: {
handleAdd() {
const {value: title } = this;
if (title === '') {
return;
}
this.odd = !this.odd;
const item = {
title,
subTitle: `${title} is ${this.odd ? 'crazy' : 'beautify'}`
}
store.commit('handAdd', item);
}
}
}
</script>
ok这种方式算是代替vuex
的一种解决方案,是不是比vuex
更简单呢,而且不用引入任何第三方库,因此在你的业务代码中可以用此来优化部分跨组件的数据通信。
provide / inject
这是一个父组件可以向子孙组件透传数据的一个属性,也就是意味着在所有子孙组件,能拿到父组件provide
提供的数据,具体可以看下下面例子
<template>
<div class="todo-list">
<h1>todo list-provide</h1>
<Search @handleAdd="handleAdd"/>
<Content />
</div>
</template>
<script>
import Search from './Search.vue';
import Content from './Content.vue';
export default {
name: 'todo-list',
components: {Search, Content},
data() {
return {
dataList: [
{
title: 'vuejs',
subTitle: 'vuejs is crazy'
},
{
title: 'reactjs',
subTitle: 'reactjs is beautify'
}
]
}
},
provide(){
return {
newDataList: this.dataList
}
},
methods: {
handleAdd(params) {
this.dataList.push(params)
}
}
}
</script>
我们在Content.vue
组件中发现
<template>
<div class="content">
<template v-for="(item, index) in newDataList">
<h1 :key="index">{{item.title}}</h1>
<h2 :key="item.subTitle">{{item.subTitle}}</h2>
</template>
</div>
</template>
<script>
export default {
inject: ['newDataList'],
}
</script>
子组件就用inject: ['newDataList']
来接收数据了。注意一点inject
一定是要与provide
组合使用,且必须是在父子组件,或者父孙,或者更深层的子组件中使用inject
。
EventBus 总线事件
这种方式平时业务上也会有用得到,特别是在表单验证中就会有
代码语言:javascript复制// utils/eventBus.js
export default class EventBus {
constructor() {
this.events = {}
}
on(name, fn) {
if (!this.events[name]) {
this.events[name] = [];
}
this.events[name].push(fn);
}
emit(name, ...payload) {
this.events[name].forEach(v => {
Reflect.apply(v, this, payload); // 执行回调函数
})
}
}
在mian.js
中挂载到prototype
上
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue';
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
import eventBus from '@/utils/eventBus';
import { store } from '@/store/index';
import App from './App'
import router from './router'
Vue.config.productionTip = false
Vue.use(Antd);
/* eslint-disable no-new */
Vue.prototype.$my_event = new eventBus;
new Vue({
el: '#app',
store,
router,
components: { App },
template: '<App/>'
});
然后在具体路由上我们看下
代码语言:javascript复制<template>
<div class="todo-list">
<h1>todo list-event-bus</h1>
<Search />
<Content :dataList="dataList"/>
</div>
</template>
<script>
import Search from './Search.vue';
import Content from './Content.vue';
export default {
name: 'todo-list',
components: {Search, Content},
data() {
return {
dataList: [
{
title: 'vuejs',
subTitle: 'vuejs is crazy'
},
{
title: 'reactjs',
subTitle: 'reactjs is beautify'
}
]
}
},
created() {
// 添加事件
this.$my_event.on('handleAdd', (payload) => {
this.dataList.push(payload);
})
}
}
</script>
在Search.vue
中我们可以看到,我们是用 this.$my_event.emit
去触发事件的
<template>
<div class="search">
<a-row type="flex" justify="center" >
<a-col :span="4">
<a-input placeholder="Basic usage" v-model="value" @pressEnter="handleAdd"></a-input>
</a-col>
<a-col :span="2">
<a-button type="dashed" @click="handleAdd">添加</a-button>
</a-col>
</a-row>
</div>
</template>
<script>
export default {
name: 'search',
data() {
return {
value: '',
odd: 0
}
},
methods: {
handleAdd() {
const {value: title} = this;
if (title === '') {
return;
}
this.odd = !this.odd;
this.$my_event.emit('handleAdd', { title,subTitle: `${title} is ${this.odd ? 'crazy' : 'beautify'}`});
}
}
}
</script>
<style>
</style>
$parent
或者$refs
访问父组件或者调用子组件方法
这是项目中比较常用粗暴的手段,用一段伪代码感受下就行,不太建议项目里用this.$parent操作
代码语言:javascript复制<template>
<div class="todo-list">
<h1>todo list-event-bus</h1>
<Search ref="search"/>
<Content :dataList="dataList"/>
</div>
</template>
<script>
import Search from './Search.vue';
import Content from './Content.vue';
export default {
name: 'todo-list',
components: {Search, Content},
data() {
return {
dataList: [
{
title: 'vuejs',
subTitle: 'vuejs is crazy'
},
{
title: 'reactjs',
subTitle: 'reactjs is beautify'
}
]
}
},
mounted() {
// 能直接调用子组件的数据或者方法
console.log(this.$refs.search.value)
}
}
</script>
在Search.vue
组件中也能调用父组件的方法
<template>
<div class="search">
<a-row type="flex" justify="center" >
<a-col :span="4">
<a-input placeholder="Basic usage" v-model="value" @pressEnter="handleAdd"></a-input>
</a-col>
<a-col :span="2">
<a-button type="dashed" @click="handleAdd">添加</a-button>
</a-col>
</a-row>
</div>
</template>
<script>
export default {
name: 'search',
data() {
return {
value: '',
odd: 0
}
},
methods: {
handleAdd() {
// 访问父类的初始化数据
console.log(this.$parent.dataList)
const {value: title} = this;
if (title === '') {
return;
}
this.odd = !this.odd;
this.$my_event.emit('handleAdd', { title,subTitle: `${title} is ${this.odd ? 'crazy' : 'beautify'}`});
}
}
}
</script>
最后把这个todo list demo
完整的完善了一下,点击路由可以切换不同todolist
了,具体可以参考code example
代码
总结
1、用具体实例手撸一个todolist
把所有vue
中涵盖的通信方式props
,自定义事件
、vuex
、vue.observable
、provide/inject
、eventBus
实践了一遍
2、明白vuex
的本质,实现了Vue.observable
跨组件通信
3、了解事件总线的实现方式,在vue中可以使用emit与on方式实现事件总线
4、本文代码示例:code example[2]
参考资料
[1]vuex
: https://v3.vuex.vuejs.org/zh/installation.html#直接下载-cdn-引用
[2]code example: https://github.com/maicFir/lessonNote/tree/master/vue/02-vue通信的几种方式/webpack