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 = {}; if (!Vue && typeof window !== 'undefined' && window.Vue) { install(window.Vue); } var plugins = options.plugins; if ( plugins === void 0 ) plugins = []; this._committing = false; this._actions = Object.create(null); this._actionSubscribers = []; this._mutations = Object.create(null); this._wrappedGetters = Object.create(null); this._modules = new ModuleCollection(options); this._modulesNamespaceMap = Object.create(null); this._subscribers = []; this._watcherVM = new Vue(); this._makeLocalGettersCache = Object.create(null); 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) { 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; 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.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)) }
|
模块的命名空间
默认情况下,模块内部的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{ 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') ...
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) => { 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 => { 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]) }) }
function registerGetter(store, type, rawGetter) { store._getters[type] = function() { return rawGetter(store.state, store.getters) } }
function registerMutation(store, type, handler) { store._mutations[type] = function(payload) { return handler.call(store, store.state, payload) } }
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 }) }
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 } }
|
参考