vuex核心图

Vue Component 负责数据的渲染,Vuex 负责数据的状态管理,Vue Component 通过dispatch函数触发 Vuex 对应action函数的执行,action函数内部调用commit函数触发对应mutation函数执行,mutation函数可访问 Vuex 的 state 对象并对其进行修改,响应式的 state 数据在被修改后触发执行 Vue Component 的render函数的重载,从而把 state 数据更新到渲染视图。

vuex是啥

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
核心思想:通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护

单例模式的实践

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
name: 'cpp'
},
mutations: {
increment(state) {
state.name='cpp + wmh'
}
}
})
export default store
// 最终引用的时候
import store from '../store'
new Vue({
router,
store,
render: (h) => h(app)
}).$mount('#app')

vuex 怎么混入到vue实例中的

vuex应用主要分两部,
第一步是通过调用Vue.use(vuex),在vue实例化过程中触发执行vuex对象的install方法,用于给后续的vue实例注入下一步创建的store对象
第二部构建的store对象以传参的形式插入vue实例,即new Vuex({store: store})

vuex的注入

查看Vue.use(plugin)方法定义,可以发现其内部会调用 plugin 的install方法。

插件一旦注册过就不需要再次注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}

查看 Vuex 源码的入口文件 index.js,install方法的定义在文件 store.js 中。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var Store = function Store (options) {
var this$1 = this;
if ( options === void 0 ) options = {};
// 如果是浏览器环境上通过 CDN 方式加载 Vue,则自动执行 install 方法
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
// store internal state
this._committing = false;
this._actions = Object.create(null); // 存储封装后的 actions 集合
this._actionSubscribers = [];
this._mutations = Object.create(null);
this._wrappedGetters = Object.create(null);
this._modules = new ModuleCollection(options); // 构建 module 对象树
this._modulesNamespaceMap = Object.create(null);
this._subscribers = [];
this._watcherVM = new Vue();
this._makeLocalGettersCache = Object.create(null);
// bind commit and dispatch to self
var store = this;
var ref = this;
var dispatch = ref.dispatch;
var commit = ref.commit;
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
};
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
};
var state = this._modules.root.state;
installModule(this, state, [], this._modules.root);
resetStoreVM(this, state);
};
...
function install (_Vue) {
// 防止 Vuex 重复装载
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 方法,如果是 Vue2 以上版本通过 mixin 使用 hook 的方式给所有组件实例注入 store 对象

1
2
3
4
5
6
function applyMixin (Vue) {
var version = Number(Vue.version.split('.')[0]);
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit });
}
}

查看vuexInit

1
2
3
4
5
6
7
8
9
10
11
12
13
function vuexInit () {
var options = this.$options;
// store injection
// 保证在任意组件访问 $store 属性都指向同一个 store 对象
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store;
} else if (options.parent && options.parent.$store) {
// 将子组件的 $store 属性指向父组件的 $store 属性上
this.$store = options.parent.$store;
}
}

通过Vue.use(Vuex)将 Vuex 以插件的形式装载进 Vue 实例中,Vue 在实例化过程中会调用 Vuex 的install方法调用Vue.mixin以 hook 的形式将 store 对象注入到 Vue 实例当中,使得可以通过访问实例的 $store 属性访问到 store 对象。

面试官问:请说出vuex原理

五个核心

Vuex的5个核心属性是什么?
分别是 state、getters、mutations、actions、modules

state: 定义应用状态的数据结构,设置初识状态
getters: 允许从组件中获取数据,mapGetters辅助函数仅仅是讲store中的getter映射到局部计算属性
Mutation:唯一更改store中状态的方法,且必须是同步函数
action: 用于提交muatation,而不是直接变更状态,阔以包含任意异步操作
module: 允许讲单一的Store拆分为多个store且同时保存在单一的状态树中

Vuex中要从state派生一些状态出来,且多个组件使用它,该怎么做?

使用getter属性,相当Vue中的计算属性computed,只有原状态改变派生状态才会改变。
getter接收两个参数,第一个是state,第二个是getters(可以用来访问其他getter)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const store = new Vuex.Store({
state: {
price: 10,
number: 10,
discount: 0.7,
},
getters: {
total: state => {
return state.price * state.number
},
discountTotal: (state, getters) => {
return state.discount * getters.total
}
},
});

然后在组件中可以用计算属性computed通过this.$store.getters.total这样来访问这些派生转态。

1
2
3
4
5
6
7
8
computed: {
total() {
return this.$store.getters.total
},
discountTotal() {
return this.$store.getters.discountTotal
}
}

Getter 怎么通过getter来实现在组件内可以通过特定条件来获取state的状态?

通过让getter返回一个函数,来实现给getter传参。然后通过参数来进行判断从而获取state中满足要求的状态。

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

然后在组件中可以用计算属性computed通过this.$store.getters.getTodoById(2)这样来访问这些派生转态。

1
2
3
4
5
6
7
8
computed: {
getTodoById() {
return this.$store.getters.getTodoById
},
}
mounted(){
console.log(this.getTodoById(2))//false
}

模块的命名空间

默认情况下,模块内部的action、mutation和getter是注册在全局命名空间,如果多个模块中action、mutation的命名是一样的,那么提交mutation、action时,将会触发所有模块中命名相同的mutation、action。
这样有太多的耦合,如果要使你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。

namespaced: true

如果么有注册命名空间,dispatch 模块内的action怎么整?

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
const state={
name: 'a.js',
path: './a.js',
moduleName: 'AA'
}

const mutations={
CHANGE_NAME: (state, name) => {
console.log('a.js中的mutation')
state.name = 'muatation: cpp-' + name
this.$store.commit('mutationA', null, { root: true })
}
}
const actions={
actionA: {
root: true,
handler(context, data) {
console.log('context', context, data)
context.dispatch('AllAction', 'from a.js')
}
},
AllAction(context, data) {
console.log('context a.js', context, data)
}
}
export default{
// namespaced: true,
state,
mutations,
actions
}

在组件中是这样触发

1
2
3
4
5
changeAction() {
this.$store.dispatch('AModule/AllAction', {
data: 'from Sort'
})
}

页面会显示报错,unknown action type: AModule/AllAction,如果启用命名空间才不会报错,当然如果使用createNamespacedHelpers创建基于某个命名空间辅助函数,则更加高效

组件注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {createNamespacedHelpers} from 'vuex'
const {mapState, mapActions} = createNamespacedHelpers('AModule') // 绑定AModule模块
...
// vue组件
methods: {
...mapActions([
'AllAction',
'actionA'
]),
changeAction() {
this.AllAction({
data: 'from Soort 222'
})
}
}

plugin插件

Vuex插件就是一个函数,它接收 store 作为唯一参数。在Vuex.Store构造器选项plugins引入。 在store/plugin.js文件中写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function createPlugin() {
return store => {
store.subscribe((mutation, state) => {
console.log('subscribe', mutation)
console.log('state', state)
}),
store.subscribeAction({
before: (action, state) => {
console.log(`before action ${action.type}`, state)
},
after: (action, state) => {//提交action之后
console.log(`after action ${action.type}`, state)
}
})
}
}

然后在store/index.js文件中写入

1
2
3
4
5
6
7
8
9
import createPlugin from './plugin'
const myPlugin = new createPlugin({name: 'myPlugin'}) // 支持传参

Vue.use(Vuex)

const store = new Vuex.Store({
...
plugins: [myPlugin],
})

vuex设计缺陷

中心化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default class Store {
constructor(options) {
this._state = options.state;
this._mutations = options.mutations;
}
get state() {
return this._vm._data.$$state
}
commit(type, payload) {
const handler = this._mutations[type]
this._withCommit(() => {
handler(payload)
})
}
}

是理解 Vuex 的核心,整份代码只有两个逻辑:
通过_state属性实现中心化、自包含数据中心层。
通过 commit 方法,回调触发事先注册的_mutations方法。
问题:

  • 状态的突变仅仅通过修改state对象属性值实现
  • 没有任何有效的机制,防止 state 对象被误修改

信号机制

Vuex 提供了两个与信号有关的接口,其源码可简略为:

1
2
3
4
5
6
7
8
9
10
class Store {
constructor() {
this.commit = function boundCommit(type, payload) {
return commit.call(store, type, payload)
}
this.dispatch = function boundDispatch(type, payload) {
return dispatch.call(store, type, payload)
}
}
}
  • 设计了两套无法正交的type体系
  • dispatch 触发的是 action 回调;commit 触发的 mutation 回调。dispatch 返回 Promise;commit 无返回值。在 action 中手误修改了 state ,而没有友好的跟踪机制(这一点在getter中特别严重)

单向数据流

这里的数据流是指从 Vuex 的 state 到 Vue 组件的props/computed/data 等状态单元的映射,即如何在组件中获取state。Vuex 官方推荐使用 mapGetter、mapState 接口实现数据绑定。

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
function resetStoreVm(store, state) {
store.getters = {}
let computed = {}
let getters = store._getters
Object.keys(getters).forEach(key => {
computed[key] = () => getters[key]()
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true
})
})
store._vm = new Vue({
data: {
$$state: state
},
computed
})
store._vm.$watch(function() {
return this._data.$$state
}, () => {
if(!store.committing) {
throw new Error('state 只能通过mutation修改')
}
}, {
deep: true,
sync: true
})
}

从代码可以看出,Vuex 将整个 state 对象托管到vue实例的data属性中,以此换取Vue的整个 watch 机制。而getter属性正是通过返回实例的 computed 属性实现的,这种实现方式,不可谓不精妙。问题则是:

  • Vuex 与 Vue 深度耦合,致使不能迁移到其他环境下使用
  • Vue 的watch机制是基于属性读写函数实现的,如果直接替换根节点,会导致各种子属性回调失效,即不可能实现immutable特性

实现一个mini版的vuex

对于理解整个vuex核心非常有帮助

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
let Vue
export class Store {
constructor(options = {}) {
this.committing = false
const {state} = options
this._actions = Object.create(null)
this._mutations = Object.create(null)
this._getters = Object.create(null)
regeistry(this, {
getters: options.getters,
mutations: options.mutations,
actions: options.actions
})
const store = this
let {
commit, dispatch
} = this
this.commit = function boundCommit(type, payload) {
return commit.call(store, type, payload)
}
this.dispatch = function boundDispatch(type, payload) {
return dispatch.call(store, type, payload)
}
resetStoreVm(this, state)
}
get state() {
return this._vm._data.$$state
}
commit(type, payload) {
const handler = this._mutations[type]
this._withCommit(() => {
handler(payload)
})
}
dispatch(type, payload) {
const handler = this._actions[type]
handler(payload)
}
_withCommit(fn) {
const committing = this.committing
this.committing = true
fn()
this.committing = committing
}
}
function resetStoreVm(store, state) {
store.getters = {}
let computed = {}
let getters = store._getters
Object.keys(getters).forEach(key => {
// 遍历 getter 配置,生成 computed 属性
computed[key] = () => getters[key]()
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true
})
})
store._vm = new Vue({
data: {
$$state: state
},
computed
})
store._vm.$watch(function() {
return this._data.$$state
}, () => {
if(!store.committing) {
throw new Error('state 只能通过mutation修改')
}
}, {
deep: true,
sync: true
})
}
function regeistry(store, options) {
Object.keys(options.mutations).forEach(type => {
registerMutation(store, type, options.mutations[type])
})
Object.keys(options.getters).forEach(type => {
registerGetter(store, type, options.getters[type])
})
Object.keys(options.actions).forEach(type => {
registerAction(store, type, options.actions[type])
})
}
// 包装getter函数
function registerGetter(store, type, rawGetter) {
store._getters[type] = function() {
return rawGetter(store.state, store.getters)
}
}
// 包装mutation
function registerMutation(store, type, handler) {
store._mutations[type] = function(payload) {
return handler.call(store, store.state, payload)
}
}
// 包装actions
function registerAction(store, type, handler) {
store._actions[type] = function(payload) {
handler.call(store, {
dispatch: store.dispatch,
commit: store.commit,
getters: store.getters,
state: store.state
}, payload)
}
}
export function install(_Vue) {
Vue = _Vue
_Vue.mixin({
beforeCreate: vuexInit
})
}
// maoState对象映射
export function mapState(states) {
const res = {}
normalizeMap(states).forEach(({key, val}) => {
res[key] = function mappingState() {
let state = this.$store.state
let getters = this.$store.getters
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
})
return res
}
export function mapGstter(states) {
const res = {}
normalizeMap(states).forEach(({key, val}) => {
res[key] = function mappingGetter() {
let state = this.$store.getters
return state[val]
}
})
return res
}
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
function vuexInit() {
const options = this.$options
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store){
this.$store = options.parent.$store
}
}

参考