用法说明
Vuex把组件的共享状态抽出来,以一个全局单例模式管理。每一个Vuex应用的核心就是store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分 状态(state)。Vuex和单纯的全局对象有以下两点不同:
- Vuex的状态存储是响应式的。当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应的得到高效更新。
- 你不能直接修改store中的状态。改变store中的状态的唯一途径就是显示地提交(commit)mutation。这样使得我们可以方便地跟踪每一个状态的变化。 解读1:Vuex的存储的数据state是响应式的,一处组件修改 state 的数据,其他用到的地方会自动变化。原理是Vuex采用了new一个Vue对象来实现数据的“响应式化”,运用Vue.js内部提供的数据双向绑定功能来实现store的数据与视图的同步更新。
解读2:修改数据通过mutation的方式,而非直接修改 store.state.count ,是因为我们想更明确地追踪到状态地变化。修改数据的入口统一起来。还有一点,如果我们使用了vue 的 devtools工具的话,直接修改 store.state.count = 100;是无法在devtools体现出来。同样地,我们在mutation中处理地是异步代码修改state的值,devtools也无法追踪到。
state
state:用来存放组件共享的数据,对数据进行初始化。 通过在根实例中注册store选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})
我们可以通过 this.$store.state 访问到定义的全局状态。一般会在子组件中通过 computed 接收。
getter
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
mutation
更改Vuex的store中的状态的 唯一 方法是提交 mutation。同时,mutation里面一般是进行同步修改state中的数据,如果我们进行了异步修改,只是devtools里面追踪不到。同时这样也不规范。
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
我们通过 store.commit('increment') 的方式调用mutations里面的方法修改state的数据。原理是发布订阅模式。
action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
我们通过 store.dispatch('increment') 来触发 actions。
mudule
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
问题
看了上面的用法说明,善于发现问题的你应该多些反问。
-
为什么 mutation 是修改 state 的唯一方式呢?直接修改 store.state.count = 100; 会怎么样呢?
页面数据 count 会变为 100, 但是 devtools 里面并没有修改还是原来的值,出现了数据不一致,无法追踪数据变化。 -
actions 里为什么也要通过 mutation 修改数据呢,直接修改数据 context.state.a = "actions"; 会怎么样呢?
页面数据 a 会变为 actions, 但是 devtools 里面并没有修改还是原来的值,出现了数据不一致,无法追踪数据变化。 -
mutation 进行异步 setTimeout 修改会怎么样呢?
页面数据会修改,但是 devtools 里面的值还是原来的并没有修改。出现了数据不一致,无法追踪数据变化。
Vuex有一种严格模式,在 new Vuex.Store({ strick: true, ...}) 的时候,开发模式下,只能通过 mutation修改state了,通过 store.state.count这种方式会报错。同时 devtools 追踪不到。只有mutation修改的state才会被devtools追踪记录下来。 同时,在mutation异步修改state的值,页面响应变化了,但是devtools的数据并没有更新。10000 和 9999. 上图:
可以看到devtools只能追踪记录通过mutation同步修改的数据
Vuex 源码分析
我们知道在使用Vuex的时候需要通过四步来完成:
- import Vuex from 'vuex'
- Vue.use(Vuex)
- 初始化 store
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)
}
}
})
- 将store实例传给vue对象
var app = new Vue({
el: '#app',
router,
store,
components: {},
data: {}
});
我们来分析关键的第二步和第三步。
Vue.use(Vuex)
我们知道 Vue.use(Vuex) 的时候会调用 Vuex定义的install方法。进入 node_modules/vuex/dist/vuex.esm.js文件中,找到 install 方法。顺藤摸瓜的调用顺序是这样的:install (_Vue) -> applyMixin(Vue) -> vuexInit 在 vuexInit 方法中,将 $store 放到组件的 this.$store中。这样在所有组件通过 this.$store 访问到定义的全局数据。
function vuexInit () {
var options = this.$options;
// store injection
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;
}
}
初始化 store
找到Store定义的 class 的构造函数,其中又有关键的三个函数:this._modules = new ModuleCollection(options);
installModule(this, state, [], this._modules.root);
resetStoreVM(this, state);
var Store = function Store (options) {
var this$1 = this;
if ( options === void 0 ) options = {};
// Auto install if it is not done yet and `window` has `Vue`.
// To allow users to avoid auto-installation in some cases,
// this code should be placed here. See #731
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
if ((process.env.NODE_ENV !== 'production')) {
assert(Vue, "must call Vue.use(Vuex) before creating a store instance.");
assert(typeof Promise !== 'undefined', "vuex requires a Promise polyfill in this browser.");
assert(this instanceof Store, "store must be called with the new operator.");
}
var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
var strict = options.strict; if ( strict === void 0 ) strict = false;
// store internal state
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);
// 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)
};
// strict mode
this.strict = strict;
var state = this._modules.root.state;
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root);
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state);
// apply plugins
plugins.forEach(function (plugin) { return plugin(this$1); });
var useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools;
if (useDevtools) {
devtoolPlugin(this);
}
};
通过debugger,我们来看一下每个函数做了哪些事情。
ModuleCollection(options);
ModuleCollection用来初始化 内部变量_modules的值。
installModule
在定义的module中,如果设置了 namespace: true 的话,通过 store.commit('computeSum') 数据的时候要加上路径的上下文。store.commit('cart/computeSum')才可以。
installModule的作用主要是为module加上namespace名字空间(如果有)后,注册mutation、action以及getter,同时递归安装所有子module。
resetStoreVM
/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
function resetStoreVM (store, state, hot) {
/* 存放之前的vm对象 */
const oldVm = store._vm
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
/* 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的computed属性 */
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
/* Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告 */
Vue.config.silent = true
/* 这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
/* 使能严格模式,保证修改store只能通过mutation */
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
/* 解除旧vm的state的引用,以及销毁旧的Vue对象 */
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
resetStoreVM首先会遍历wrappedGetters,使用Object.defineProperty方法为每一个getter绑定上get方法,这样我们就可以在组件里访问this.$store.getters.test就等同于访问store._vm.test。
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
之后Vuex采用了new一个Vue对象来实现数据的“响应式化”,运用Vue.js内部提供的数据双向绑定功能来实现store的数据与视图的同步更新。
store._vm = new Vue({
data: {
$$state: state
},
computed
})
这时候我们访问store._vm.test也就访问了Vue实例中的属性。
通过这一步,我们将state的数据通过new Vue()后,转换为响应式的。同时,将 getters 里面定义的数据通过 new Vue 的computed实现了计算属性的特点,只有当它的依赖值发生了改变才会被重新计算。
利用Vue自己实现 Vuex
我们可以自定义一个 MyStore.js,里面定义项目共享的数据项以及修改数据的方法。然后挂载在根data中。
// MyStore.js
export default class{
constructor() {
this.count = 10;
this.size = 100;
let self = this;
this.mutations = {
incrementCount(){
self.count = 200;
},
incrementSize(){
self.size++;
}
}
}
}
项目引入:
import MyStore from './MyStore.js';
var app = new Vue({
el: '#app',
components: {},
data: {
myStore: new MyStore()
}
});
这样,我们在组件中可以通过 this.$root.myStore.count 访问到全局变量 count 了。通过 this.$root.myStore.mutations.incrementCount(); 进行变量的修改。
原生 JavaScript 实现 state 状态管理系统
www.cnblogs.com/zhangycun/p… 这篇博客介绍了用 JavaScript 原生代码实现 数据管理的一个方法。利用的是 Proxy + 发布订阅模式。数据改变的时候,通知所有订阅者,render页面。
最后
Vuex中的数据是存储在浏览器内存中的,当刷新页面后会导致数据清空。这个时候要在window beforeunload时候存入 sessionStorage 中,在页面created 或者 beforeCreate 的时候从 sessionStorage 中读取数据。
export default {
name: 'App',
created () {
//在页面加载时读取sessionStorage里的状态信息
if (sessionStorage.getItem("store") ) {
this.$store.replaceState(Object.assign({}, this.$store.state,JSON.parse(sessionStorage.getItem("store"))))
}
//在页面刷新时将vuex里的信息保存到sessionStorage里
window.addEventListener("beforeunload",()=>{
sessionStorage.setItem("store",JSON.stringify(this.$store.state))
})
}
}