前言:本篇文章主要对vue组件之间的通信方式进行总结,并且探索vuex状态管理的核心概念和基本使用,以及自己模拟实现一个vuex。并不是任何项目都适合使用vuex,只有中大型项目才适合使用vuex。
一、vue 组件间通信方式
组件间通信方式有三种
(一)父组件给子组件传值
子组件中通过 props 接收数据
props
的定义
👁️🗨️ 可以是一个数组,数组中的元素是要接受的数据标识符,是字符串的形式,例如:props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
👁️🗨️ 也可以是一个对象,key
是要接受的数据标识符,value
是数据类型
<template>
<h1>{{ msg }}</h1>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
父组件中给子组件通过相应属性传值
1、通过import
引入子组件
2、通过components
注册子组件
3、在模版中使用子组件,通过属性传值
可以传递静态值,也可以使用v-bind传递动态数据
<template>
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'HomeView',
components: {
HelloWorld
}
}
</script>
(二)子组件给父组件传值
子组件给父组件传值需要使用自定义事件。自定义事件,就是类似于click
、keypress
等事件,使用v-on
监听。只不过内置的事件通过鼠标等操作触发,自定义事件需要使用vm.$emit()
方法手动触发。首先需要确定一个自定义事件的名称,这里我们叫message
。
父组件
在使用子组件的时候,使用v-on
监听自定义事件,并且设置回调函数。注意v-on:message
绑定的是回调函数的定义,而不是调用哦,不能加小括号。
v-on:
可以简写为@
<template>
<HelloWorld v-on:message="onPostMessage"/>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'HomeView',
components: {
HelloWorld
},
methods: {
// 自定义事件的回调处理函数
onPostMessage(message) {
console.log(message)
}
}
}
</script>
子组件
自定义触发事件的时机,例如在按钮点击的时候触发自定义事件。vm.$emit()
接受两个参数,参数一是自定义事件的名称,参数二是要传递给自定义事件的参数。
<template>
<div>
<button v-on:click="postMessage">给父组件传值</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
methods: {
postMessage() {
this.$emit('message', 'Hello World')
}
}
}
</script>
(三)不相关组件间的传值
这种方法是通用的组件传值方法。需要创建一个额外的的Vue实例,这里管它叫bus
,用来管理自定义事件,通常定义到eventBus.js文件中
eventBus.js
import Vue from "vue";
const bus = new Vue();
export default bus;
发出数据的组件需要使用bus.$emit()
来触发自定义事件
发出数据的组件
<template>
<div class="about">
<button @click="postMessage">给其他组件传值</button>
</div>
</template>
<script>
import bus from '@/eventBus.js'
export default {
name: 'AboutView',
methods: {
postMessage() {
// 触发自定义事件
bus.$emit('message', 'Hello World')
}
}
}
</script>
接收数据的组件需要使用bus.$on()
监听自定义事件
<template>
<h1>HomeView</h1>
</template>
<script>
// @ is an alias to /src
import bus from '@/eventBus.js'
export default {
name: 'HomeView',
created() {
// 监听自定义事件
bus.$on('message', this.onPostMessage)
},
methods: {
onPostMessage(message) {
console.log(message)
}
}
}
</script>
二、vuex 核心概念和基本使用
• vuex 是专门为 vue.js 设计的状态管理库
• vuex 采用集中式的方式存储需要共享的状态
• vuex 的作用是进行状态管理,解决复杂组件通信,数据共享
• vuex 集成到了devtools 中,提供了 time-travel 时光旅行、历史回滚功能
• Vuex Time-travel 是一个 Vuex 插件,用于在 Vue.js 应用程序中实现状态的时间旅行功能。时间旅行是指能够回溯和查看应用程序状态的历史记录。在开发过程中,这对于调试和理解状态变化非常有用。Vuex Time-travel 插件通过记录每个状态变化的快照,允许开发者在不影响当前应用程序状态的情况下,回溯到过去的状态。
(一)Vuex核心概念
Vuex集中式管理全局状态,首先需要一个管理状态的仓库,通常称为Store仓库,每一个应用只有一个Store。Store中存储着几个变量,包括State、Getter、Mutation、Action、Module。
- State存放需要统一管理的全局状态,并不是所有的变量都需要存放到State中,单个组件中维护的变量还是可以放在组件中单独维护。
- Getter类似于计算属性,其中的定义是函数的形式,函数的返回值是对于某个全局状态进行处理后的结果。
getters: {
doneTodos (state) {
return state.todos.filter(todo => todo.done)
}
}
- Mutation类似于事件,这是更改 Vuex 的
store
中的状态的唯一的地方。 - Action的定义类似于Mutation,其中可以进行各种操作,允许异步操作。
- Module当应用变得非常复杂时,store 对象就有可能变得相当臃肿。Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
下面这张图很好的解释了Vuex的几个核心概念之间的关系。state
是我们管理的全局状态,我们可以将全局状态渲染到组件Vue Components中。当在组件内部进行一些操作需要修改全局状态的时候,就需要通过dispatch()
方法触发action
中定义的事件,俗称”分发 Action“。action
中定义的事件可以进行异步的操作,比如向服务器请求数据,以及进行其他的操作。当需要修改state
的时候,通过commit()
方法”提交Mutation“。在Vuex中,只有mutation
中定义的方法能直接修改state
中的数据,mutation
中的修改是同步的。
(二)Vuex基本使用
1、基本结构
我们在使用vue create 项目名
创建Vue项目的时候,vue-cli会提示我们是否安装vuex,只要选择了安装,vue-cli就会帮我们安装好vuex,并且初始化一个store目录,还为我们初始化好了一份基础代码
src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
// 注册插件
Vue.use(Vuex)
// 创建并导出Vuex.Store对象
export default new Vuex.Store({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})
在src/main.js
中也为我们引入vuex并且给Vue实例注入store选项
import store from './store'
new Vue({
store,
render: h => h(App)
}).$mount('#app')
2、State用法
假设在State中定义了两个全局状态
src/store/index.js
state: {
name:'zs',
age:18
},
🟠 基础使用:vuex会将$store
直接加到Vue实例上,所以在组件中可以直接通过$store
使用State中的数据,这种方式不需要使用import
引入store
<template>
<div class="about">
{{$store.state.name}}
{{$store.state.age}}
</div>
</template>
🟠 更加方便的方式:上边的使用方式如果组件使用到很多的数据,那么$store.state
这个前缀就需要重复很多次。vuex为我们提供了一个mapState()
方法,这个方法类似于计算属性,可以将state
中存储的数据转换为当前组件实例上的数据,并且保证数据的响应式。
mapState()
的使用有两种传参方式,
一种是传一个数组,数组中存放字符串形式的数据描述符,要使用哪个属性,就增加哪个属性;
mapState(['name','age'])
第二种是传递一个对象,这种方式支持对state
中的属性进行重命名,避免和当前组件中的属性重名,key是重命名后的属性名,value是一个方法,接收state
,返回对应属性。在这个方法里也可以对目标属性做一些操作并且返回操作后的结果。
mapState({
myname:state=>state.name,
myage:state=>state.age
})
mapState()
的返回值是对象的形式
在组件的computed
计算属性中,要对属性进行重新计算,就需要定义成属性名:处理函数
的形式。所以这里需要将mapState()
的返回值解构赋值放到computed
中。在模版中就可以直接使用重命名后的属性名获取state
中的数据
<template>
<div class="about">
{{myname}}
{{myage}}
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'AboutView',
computed:{
// ...mapState(['name','age'])
...mapState({
myname:state=>state.name,
myage:state=>state.age
})
}
}
</script>
3、Getter用法
Getter的定义的形式是函数,接收state
对象作为参数,对某个属性进行计算,返回处理后的结果。
getters: {
getAgeAdd10(state){
return state.age + 10
}
},
在使用的时候,直接通过$store.getters.方法名
的形式就可以获取计算后的属性,并不需要将定义的这个函数执行再赋值。
<div class="about">
{{myname}}
{{$store.getters.getAgeAdd10}}
</div>
Getters
中的属性也可以像State
一样,通过Vuex提供的mapGetters()
方法映射到组件实例的计算属性中,mapGetters
的使用类似于mapState()
。
<template>
<div class="about">
{{myname}}
{{getAgeAdd10}}
</div>
</template>
<script>
import {mapState,mapGetters} from 'vuex'
export default {
name: 'AboutView',
computed:{
...mapState({
myname:state=>state.name
}),
...mapGetters([
'getAgeAdd10'
])
}
}
</script>
4、Mutation用法
Mutation里面存储的是对State进行处理的处理函数。Mutation类似于注册事件的作用,注册的事件都是同步事件,因为需要确保获取的State是实时的State。Mutation中注册的事件是修改State的唯一的地方。使用Mutation注册的事件不能直接调用事件,而是需要使用Store的commit()
方法,提交事件
store/index.js
mutations: {
// payload 载荷,是调用该方法时传递的额外参数
changeName(state, payload) {
state.name = payload
}
},
组件实例中
<template>
<div>
<div class="about">
{{name}}
</div>
<button @click="changeName">changeName</button>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'AboutView',
computed:{
...mapState(['name'])
},
methods:{
changeName(){
this.$store.commit('changeName','lisi')
}
}
}
</script>
类似于mapState
、mapGetters
,vuex也提供了Mutation映射到当前组件实例上。但是需要注意的是,Mutation中的事件应该映射到methods
中,调用方法只需要穿一个额外参数就可以,默认state
会作为第一个参数传递给Mutation事件。
<template>
<div>
<div class="about">
{{name}}
</div>
<button @click="changeName('lisi')">changeName</button>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'AboutView',
computed:{
...mapState(['name'])
},
methods:{
...mapMutations(['changeName']),
}
}
</script>
5、Action用法
Action中的方法可以进行各种异步操作,最后修改State需要提交Mutation。
store/index.js
actions: {
// context 上下文,相当于 store 对象
changeNameAsync(context, payload) {
setTimeout(() => {
// 提交Mutation,调用Mutation中注册的事件修改State
context.commit('changeName', payload)
}, 2000)
}
},
使用Action中的方法需要使用$store.dispatch(方法名,参数)
方法
<template>
<div>
<div class="about">
{{name}}
</div>
<button @click="changeNameAsync">changeName</button>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'AboutView',
computed:{
...mapState(['name'])
},
methods:{
changeNameAsync(){
this.$store.dispatch('changeNameAsync','lisi')
},
}
}
</script>
类似的还有mapActions
<template>
<div>
<div class="about">
{{name}}
</div>
<button @click="changeNameAsync('lisi')">changeName</button>
</div>
</template>
<script>
import {mapState, mapActions} from 'vuex'
export default {
name: 'AboutView',
computed:{
...mapState(['name'])
},
methods:{
...mapActions(['changeNameAsync'])
}
}
</script>
6、Module用法
如果项目非常大,全局状态特别多,使用一个状态树进行数据管理就会非常的大。此时我们可以将数据分割成多个模块,每个模块负责不同的功能。例如下列示例项目中有两个组件,一个是About.vue,一个是Home.vue,分别给他们依赖的数据创建一个模块,文件目录结构如下:
模块文件基本内容store/home.js
const state = {}
const getters = {}
const mutations = {}
const actions = {}
const modules = {} // 如果有嵌套子模块
export default {
state,
getters,
mutations,
actions,
modules
}
然后需要在vuex的根文件中注册模块
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import about from './modules/about'
import home from './modules/home'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
about,
home
},
})
console.log(store)
export default store
此时会将about
,home
这两个模块挂载到store.state
上,可以通过store.state.about
访问模块中的属性。模块中的方法会直接挂载到store._mutations
身上。我们打印一下store
,可以看到定义在模块中的Mutations和Actions会和主模块中的Mutations和Actions合并,一起挂载到_mutations
和_actions
属性上。
在组件实例的模版中使用模块中的状态时,需要通过$store.state.模块名.状态名
使用,提交模块中的Mutation时,只需要通过$store.commit('方法名',参数)
来提交。下面例子在about
模块中定义了全局状态name
和Mutation方法changeName
组件实例
<template>
<div>
<div class="about">
{{$store.state.about.name}}
</div>
<button @click="$store.commit('changeName','lisi')">changeName</button>
</div>
</template>
<script>
export default {
name: 'AboutView'
}
</script>
如果多个模块中都有changeName
这个Mutation方法,就会一次执行所有的同名方法。例如下面代码,在About
和Home
模块中都有changeName
这个Mutation方法
store/modules/about.js
const state = {
name: 'zs',
age: 18
}
const mutations = {
// payload 载荷,是调用该方法时传递的参数
changeName(state, payload) {
console.log('about changeName')
console.log(state)
state.name = payload
}
}
store/modules/home.js
const state = {}
const mutations = {
changeName(state, payload) {
console.log('home changeName')
console.log(state)
state.name = payload
}
}
点击按钮提交changeName
的时候,控制台输出:
两个模块中的同名的Mutation方法都执行了,但是接收的state
是各自模块的state
。
这样的话代码就不太清晰,我们在组件实例中不知道执行的是哪个模块的Mutation方法。为了明确我们要使用的是哪个模块中的方法,我们可以在定义模块的时候,给模块增加一个属性namespaced: true,
所有模块导出的位置
export default {
namespaced: true,
state,
getters,
mutations,
actions,
modules
}
先讲一下mapState
的另一种用法,接受两个参数,第一个参数用来指定属性的模块,第二个属性用来指定要映射的属性;mapMutation
也类似
组件实例中
<template>
<div>
<div class="about">
{{name}}
</div>
<button @click="$store.commit('changeName','lisi')">changeName</button>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
export default {
name: 'AboutView',
computed: {
...mapState('about', ['name'])
},
methods: {
...mapMutations('about', ['changeName'])
}
}
</script>
7、Vuex严格模式
在使用Vuex的过程中我们约定修改全局状态只在Mutation方法中修改,但这仅仅是一个约定,程序并没有限制,仍然可以在组建实例中获取state
并重新赋值。对此Vuex提出了严格模式的概念,当开启了严格模式之后,在组件实例中修改全局状态就会报错。开启严格模式只需要在创建store
的时候,增加一个配置项strict: true
,需要注意的是,不要在生产环境下开启严格模式,因为严格模式会深度检查状态变更导致性能缓慢。所以这个配置项可以设置为strict: process.env.NODE_ENV !== 'production'
,让构建工具自动检测环境来决定是否开启严格模式。当开启严格模式之后,我们在组件实例的created生命周期中去修改模块的状态:
created () {
console.log(this.$store)
this.$store.state.about.name = 'zhangsan'
},
控制台就会报出错误信息
但即使是报错了,数据也已经修改了
8、Vuex插件
Vuex插件插件就是一个函数,接收store
作为参数,稍微看一下vuex源码中对plugins
做了什么,就是plugins
遍历执行,并且把当前store
对象作为参数传递给每一个plugin
var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
// apply plugins this$1就是当前store对象
plugins.forEach(function (plugin) { return plugin(this$1); });
Vuex提供了subscribe
方法,通过store.subscribe()
方法订阅了状态的变化。subscribe
方法的回调函数接收两个参数:mutation
和state
。mutation
参数是一个描述mutation
的对象,包含了type、payload等属性。state参数是当前的状态对象。在每次Mutation
方法执行完毕就会触发store.subscribe()
订阅的方法。
插件的注册有两步,初始化和注册
const myPlugin = store => {
// 当 store 初始化后调用
store.subscribe((mutation, state) => {
// 每次 mutation 之后调用
// mutation 的格式为 { type, payload }
console.log('myPlugin')
console.log(mutation)
console.log(state)
})
}
const store = new Vuex.Store({
// 省略若干行代码
plugins: [myPlugin]
})
每个Mutation方法执行完毕都会触发这个回调函数
适用于在数据变化后都需要执行的操作,比如缓存最新状态、提交修改
三、模拟实现 vuex
(一)结构分析
要实现一个自己的vuex,首先需要分析一下vuex的结构和功能。
👾 首先vuex是一个被导出的对象,可以通过import
导入
👾 vuex具有install()
方法,可以通过Vue.use()
安装注册
👾 vuex具有Store构造方法,可以通过new Vuex.Store()
创建store
实例,store
实例具有state
、getters
、mutations
等属性
👾 Store
实例具有commit()
方法和dispatch()
方法,分别用来触发mutations
、actions
中的函数
基本结构就是拥有一个Store
类和一个install()
方法,install()
方法要接收Vue构造函数作为参数,并且需要一个变量来保存Vue构造函数
src/myvuex/index.js
// 用于保存Vue
let _Vue = null;
class Store {
}
function install(Vue) {
_Vue = Vue;
}
export default {
Store, install
}
(二)install()
每一个Vue插件都需要有一个install()
方法。首先我们要弄明白install()
方法要做什么事情。在任何一个组件模版中,我们都可以通过下面的方法通过组件实例的$store
属性访问vuex定义的数据
<button @click="$store.commit('changeName','lisi')">changeName</button>
所以,install()
方法应该将vuex
定义的数据挂载到Vue实例上,要挂载到每一个Vue实例上,就需要挂载到Vue.prototype
上,直接通过原型链让所有的Vue实例继承。由于install()
方法执行的时候,Vue根组件可能还没有实例化,所以要使用混入,在Vue实例的beforeCreate
生命周期执行挂载操作,此时就是根组件的beforeCreate
生命周期。
function install(Vue) {
_Vue = Vue;
// 把$store加入到所有的Vue实例上
_Vue.mixins({
beforeCreate() {
if (this.$options.store) {
_Vue.prototype.$store = this.$options.store
}
}
})
}
(三)Store类
Store类首先需要一个构造函数,接收选项对象options
,options
中包含store
实例的属性。首先通过结构赋值获取这几个属性,并且设置一个初始值{},避免没有传入参数是为空。
constructor(options) {
// 解构赋值获取用户传入的选项,如果没有传入就初始化为{}
const {
state = {},
getters = {},
mutations = {},
actions = {}
} = options
}
这几个属性分别进行什么处理呢?我们要从属性的使用方法入手。
① state
属性是一个响应式数据,可以直接在模版中绑定,所以我们使用_Vue.observable()
方法,将state
转换成响应式数据,并存放到Store
实例的state
属性上。
this.state = _Vue.observable(state)
② getters
是一个对象,里面存放的是很多方法,这些方法接收state
作为参数,通过对state
中的某个属性做简单的处理然后返回。getters
的使用,是直接通过属性名的方式访问,当访问该属性的时候,才会执行处理方法。虽然用户传进来的getters
的属性是函数,但是我们要通过Object.defineProperty()
将方法转换为getter/setter
存储到getters
中,并且在该属性的get
访问器中,执行用户传进来的方法,返回处理后的结果。
this.getters = Object.create(null)
// getters是用户传进来的getters,里面定义的都是方法
Object.keys(getters).forEach(key => {
// key:用户定义的方法名
Object.defineProperty(this.getters, key, {
// 获取属性的时候获取的其实是get指定的函数返回的结果
get: () => getters[key](state)
})
})
③ mutations
、actions
mutations
、actions
存储为私有属性
this._mutations = mutations;
this._actions = actions;
④ commit()
和dispatch()
commit()
方法用来提交Mutation,其实就是找到Mutation中定义的方法,然后执行。接收两个参数,第一个参数是要执行的Mutation函数的方法名,第二个参数是要传递给Mutation方法的参数。另外,Mutation方法统一都是接收state作为第一个参数,commit()
指定的参数作为第二个参数。
// commit方法,用来提交mutation
commit(type, payload) {
this._mutations[type](this.state, payload)
}
dispatch()
方法和commit()
方法类似,用来分发Action
,区别在于,Action
方法接收的第一个参数不是全局状态State
,而是context
上下文,这里为了简便,直接传递了this
,也就是当前Store
实例。
// dispatch方法,用来分发action
dispatch(type, payload) {
this._actions[type](this, payload)
}