Vuex 框架核心流程

Vuex 框架核心流程

本文章节如下

  • 首先先来看看 Vuex 的整体流程
  • 然后介绍一些比较常见的 API 的用法
  • 最后则是介绍一下初始化装载与注入的过程

至于源码的部分,则是会下一篇章节当中来进行介绍

流程图

Vuex 的整个执行流程如下(转自网络)

VuexVue Components 建立起了一个完整的生态圈,包括开发中的 API 调用一环,围绕这个生态圈,简要介绍一下各模块在核心流程中的主要功能

  • Vue Components
    • Vue 组件,HTML 页面上负责接收用户操作等交互行为
    • 执行 dispatch 方法触发对应 action 进行回应
  • dispatch
    • 操作行为触发方法,是唯一能执行 action 的方法
  • actions
    • 操作行为处理模块,负责处理 Vue Components 接收到的所有交互行为,包含同步或者异步的操作
    • 支持多个同名方法,按照注册的顺序依次触发,向后台 API 请求的操作就在这个模块中进行,包括触发其他 action 以及提交 mutation 的操作
    • 该模块提供了 Promise 的封装,以支持 action 的链式触发
  • commit
    • 状态改变提交操作方法,对 mutation 进行提交
    • 是唯一能执行 mutation 的方法
  • mutations
    • 状态改变操作方法,是 Vuex 修改 state 的唯一推荐方法,其他修改方式在严格模式下将会报错
    • 该方法只能进行同步操作,且方法名只能全局唯一,操作之中会有一些 Hook 暴露出来,以进行 state 的监控等
  • state
    • 页面状态管理容器对象,集中存储 Vue componentsdata 对象的零散数据
    • 全局唯一,以进行统一的状态管理,页面显示所需的数据从该对象中进行读取,利用 Vue 的细粒度数据响应机制来进行高效的状态更新
  • getters
    • state 对象读取方法,图中没有单独列出该模块,应该被包含在了 render
    • Vue Components 通过该方法读取全局 state 对象

总结如下

  • Vue 组件接收交互行为,调用 dispatch 方法触发 action 相关处理
  • 若页面状态需要改变,则调用 commit 方法提交 mutation 修改 state
  • 通过 getters 获取到 state 新值,重新渲染 Vue Components,界面随之更新

目录结构

目录 介绍
module 提供 module 对象与 module 对象树的创建功能
plugins 提供开发辅助插件,如时光穿梭功能,state 修改的日志记录功能等
helpers.js 提供 actionmutations 以及 getters 的查找 API
index.js 是源码主入口文件,提供 store 的各 module 构建安装
mixin.js 提供了 storeVue 实例上的装载注入
util.js 提供了工具方法如 finddeepCopyforEachValue 以及 assert 等方法

下面我们就来看看其中的一些 API 的用法,比如之前介绍过的 StateGetterMutationaction 等,Vuex 使用单一状态树,每个应用将仅仅包含一个 store 实例,从 store 实例中读取状态最简单的方式就是在计算属性当中返回某个状态

State

Vuex 通过 store 选中,将状态从根组件注入到每一个子组件当中

1
2
3
4
5
6
new Vue({
el: '#app',
store,
components: { Counter },
template: `<div><counter></counter></div>`
})

子组件可以通过 this.$store 访问到该 store 的实例

1
2
3
4
5
6
7
8
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count() {
return this.$store.state.count
}
}
}

Getter

简单的来说,可以将其理解为 store 的计算属性,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算,它接收 state 作为『第一个参数』

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = new Vuex.store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false },
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})

外部调用

1
store.getters.doneTodos  // [{ id: 1, text: '...', done: true }]

也可以接受其他的 getter 作为『第二个参数』

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
getters: {
// ...

doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}

store.getters.doneTodosCount // 1


// 在组件当中使用
computed: {
doneTodosCount() {
return this.$store.getters.doneTodosCount
}
}

也可以返回一个函数,来实现给 getter 传参

1
2
3
4
5
6
7
8
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}

store.getters.getTodoById(2) // { id: 2, text: '...', done: false }

Mutation

更改 Vuexstore 中的状态的唯一方法就是提交 mutation,非常类似于事件

  • 每个 mutation 都有一个事件类型(type)和一个回调函数(handler
  • 回调函数就是进行状态更改的地方,并且接收 state 作为第一个参数
1
2
3
4
5
6
7
8
9
10
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment(state) {
state.count++
}
}
})

需要注意

  • 不能直接调用 mutation handle
  • 应当以相应的 type 调用 store.commit 方法
1
store.commit('increment')

同时可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
mutations: {
increment(state, payload) {
state.count += payload.amount;
}
}

store.commit('increment', {
amount: 10
})

// 等同于

store.commit({
type: 'increment',
payload: 10
})

Mutation 的提交

  • 必须是同步函数(若是异步,则可能存在当 mutation 触发的时候,回调函数还没有被调用的情况)
  • mutation 的提交可以使用 this.$store.commit('...')
  • 或者可以使用 mapMutations 辅助函数将组建中的 methods 映射为 store.commit 调用,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { mapMutations } from 'Vuex';

export default {
// ...
methods: {
...mapMutations([
// 将 this.increment() 映射为 this.$store.commit('increment')
'increment',

// 将 this.incrementBy(amount) 映射为 this.$store.commit('incrementBy', amount)
'incrementBy'
]),
...mapMutations({
// 将 this.add() 映射为 this.$store.commit('increment')
add: 'increment'
})
}
}

Action

  • 提交的是 mutation,而不是直接变更状态
  • 可以包含任意异步操作

一个简单的 action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const store = new Vuex.store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
increment(context) {
context.commit('increment')
}
}
})

action 函数接受一个与 store 实例具有相同方法和属性的 Context 对象,因此可以调用 context.commit 提交一个 mutation

分发 Action

action 通过 store.dispatch 方法触发

1
store.dispatch('increment')

之所以这样使用,是因为 mutation 必须同步执行,而 action 则不必如此,可以在其内部执行异步操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
actions: {
increment({ commit }) {
setTimeout(_ => {
commit('incrementAsync')
}, 1000)
}
}

// 同时也支持载荷方式
store.dispatch('incrementAsync', {
amount: 10
})

// 等同于
store.dispatch({
type: 'incrementAsync',
amount: 10
})

组件当中 Action 的分发

Mutation 类似,可以使用 this.$store.dispatch('...') 或者使用 mapActions 辅助函数

组合 Action

因为 store.dispatch 返回的是一个 Promise 对象,所以可以使用 then() 方法来进行处理,亦或是可以使用 async/await

1
2
3
4
5
6
7
8
9
10
11
// 假设 gotData() 与 gotOtherData() 均返回 Promise
actions: {
async actionA({ commit }) {
commit('gotData', await gotData())
},
async actionB({ commit }) {
// 等待 actionA 完成
await dispatch('actionA')
commit('gotOtherData', await gotOtherData())
}
}

初始化装载与注入

下面我们就来看看 Vuex 到底是如何在项目当中进行装载与注入的,首先是入口文件,先来看入口处的 export 函数到底导出了哪些东西,详细可以见官方 vuejs/vuex

需要注意的是,可能版本不同而导致内容有所不同,但是我们关心的仅仅是几个核心方法

1
2
3
4
5
6
7
8
9
// https://github.com/vuejs/vuex/blob/dev/src/index.js
export default {
Store,
install,
mapState,
mapMutations,
mapGetters,
mapActions
}

装载与注入

我们一般在使用 Vuex 的时候如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// 创建并导出 store 对象
export default new Vuex.Store()


// index.js 文件引入
import Vue from 'vue'
import App from './../pages/app.vue'
import store from './store.js'

new Vue({
el: '#root',
router,
store, // <== 这里注入
render: h => h(App)
})

除了 Vue 的初始化代码,只是多了一个 store 对象的传入,我们来看下源码中的实现方式

1
2
3
4
5
6
7
8
// store.js
// 定义局部变量 Vue,用于判断是否已经装载和减少全局作用域查找
let Vue

// 判断若处于浏览器环境下且加载过 Vue,则执行 install 方法
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}

若是首次加载,将局部 Vue 变量赋值为全局的 Vue 对象,并执行 applyMixin 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// store.js
export function install(_Vue) {
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}

下面是 applyMixin 的源码,如果是 2.x 以上的版本,可以使用 Hook 的形式进行注入,即在 beforeCreated 钩子前插入初始化代码(vuexInit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])

if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}

/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit() {
const options = this.$options
// 将初始化 Vue 根组件时传入的 store 设置到 this 对象的 $store 属性上
// 子组件从其父组件引用 $store 属性,层层嵌套进行设置
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}

这也就是为什么我们在 Vue 的组件中可以通过 this.$store.xxx 来访问到 Vuex 的各种数据和状态的原因了,因为在任意组件中执行 this.$store 都能找到装载的那个 store 对象,如下图所示,页面的结构为

对应的 store 流向

# Vue

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×