在业务代码中常用到的Vue数据通信方式

2022-07-28 12:31:51 浏览数 (2)

​​

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.vueprops就是:dataList="dataList"Content.vue中我们可以看到就是通过props上的dataList获取父组件数据的。

代码语言:javascript复制
<!--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中我们引入对应逻辑

代码语言:javascript复制
<!--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'}`
      })
    }
  }
}

我们可以看到自定义事件子组件中就是这么给父组件通信的

代码语言:javascript复制
...
this.$emit('handleAdd', {
        title,
        subTitle: `${title} is ${this.odd ? 'crazy' : 'beautify'}`
})
.sync实现props的双向数据通信

在vue中提供了.sync修饰符,本质上就是便捷处理props单向数据流,因为有时候我们想直接在子组件中修改props,但是vue中是会警告的,如果实现props类似的双向数据绑定,那么可以借用.sync修饰符,这点项目里设计弹框时经常有用。

同样是上面todolist的例子

代码语言:javascript复制
<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中可以看到

代码语言:javascript复制
...
<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))

代码语言:javascript复制
...
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修饰符简化业务代码,实际与下面代码等价

代码语言:javascript复制
<Search :dataList="dataList" @update="update"/>
vuex

vuex在具体业务中基本上都有用,我们看下vuex是如何实现数据通信的,关于`vuex`[1]如何使用参考官方文档,这里不讲如何使用vuex,贴上关键代码

代码语言:javascript复制
// 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中引入

代码语言:javascript复制
// main.js
...
import {store} from '@/store/index';
...
/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  router,
  components: { App },
  template: '<App/>'
})

我们看下主页面路由页面,现在变成这样了,父组件没有任何props自定义事件,非常的干净。

代码语言:javascript复制
<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.vueContent.vue组件

代码语言:javascript复制
<!--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就行了

代码语言:javascript复制
<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的一些思想,具体代码如下

代码语言:javascript复制
// 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

代码语言:javascript复制
<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

代码语言:javascript复制
<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提供的数据,具体可以看下下面例子

代码语言:javascript复制
<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组件中发现

代码语言:javascript复制
<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

代码语言:javascript复制
// 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去触发事件的

代码语言:javascript复制
<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组件中也能调用父组件的方法

代码语言:javascript复制
<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,自定义事件vuexvue.observableprovide/injecteventBus实践了一遍

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

0 人点赞