1. vuex 是什么?
Vuex 集中式 存储管理应⽤的所有组件的状态,并以相应的规则保证状态以 可预测 的⽅式发⽣变化。 为什么是可预测?可预测 是为了能在改变前或改变后进行一些操作。
什么样的数据会存放在 vuex 的 store 中呢?
- 一般 跨组件共享的数据 才会存放到 vuex 中
- 组件之间的状态共享
先来看一下下面这张图(大家应该都不陌生) —— 单向数据流的图:
一般情况下单向数据流会更加简洁,更容易找问题,如果数据中出现问题更加容易调试。
vuex 中有个非常特殊的类叫 store,所有数据都在 store 这个类中去存储。存储器 store 中的状态(state)是响应式的,所以当用户触发 mutations 时,修改 state 的数据,会让使用它的组件重新 render。
2. vuex 的使用示例
安装依赖:npm install vuex
import Vue from 'vue';
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {},
// 改变状态只能通过 mutations
mutations: {},
// actions:可以做一些非常复杂的业务逻辑
actions: {},
modules: {},
})
src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
counter: 1,
},
mutations: {
// state 从何而来?
add(state){
state.counter++
},
},
actions: {
syncAdd({ commit }){
setTimeout(() => {
commit('add')
}, 1000)
},
},
modules: {},
})
访问全局状态:
<template>
<div>
<p @click="$store.commit('add')">{{ $store.state.counter }}</p>
<p @click="$store.dispatch('syncAdd')">{{ $store.state.counter }}</p>
</div>
</template>
3. 需求:
- 实现 Store 类:
- 需要有一个响应式状态 state
- 需要一个改状态的 commit() 方法
- 复杂业务逻辑的 dispatch() 方法
- getters
- 挂载 router 一样,可以参考 vue2源码学习(1) - 手写 vue-router)
4. 开始
在开始前先准备两个文件:src/myStore/index.js 、 src/myStore/myVuex.js
4.1 src/myStore/index.js
import Vue from 'vue'
import Vuex from './myVuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
counter: 1
},
mutations: {
// state从何而来?
add(state) {
state.counter++
}
},
actions: {
// context: {commit, dispatch, state, rootState}
add({commit}) {
setTimeout(() => {
commit('add')
}, 1000);
}
},
})
4.2 src/myStore/myVuex.js
// 1.实现插件
let Vue
class Store {
constructor(options) {
this._mutations = options.mutations
this._actions = options.actions
// 需要对options.state做响应式处理
// Vue.util.defineReactive()
this._vm = new Vue({
data() {
return {
// $$state是不会代理到_vm上的
$$state: options.state
}
},
})
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}
get state() {
return this._vm._data.$$state
}
set state(v) {
console.error('please use replaceState reset state');
}
// 修改状态
// store.commit('add', 1)
commit(type, payload) {
const entry = this._mutations[type]
if (!entry) {
console.error('unknown mutation: ' + type);
return
}
entry(this.state, payload)
}
dispatch(type, payload) {
const entry = this._actions[type]
if (!entry) {
console.error('unknown action: ' + type);
return
}
entry(this, payload)
}
}
function install(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store
}
}
})
}
export default { Store, install };
4.3 对 myVuex.js
内容进行简单解说
先看一下 vuex 使用结构:
Vue.use(Vuex)
export default new Vuex.Store({
})
以此可以看出 myVuex.js 插件的名字叫 Vuex 而不是叫 store。
根据 vue2源码学习(1) - 手写 vue-router 的学习,可以得知 myVuex 插件的基本结构应该是:
class Store {
constructor(options){
}
}
function install(_Vue) {
}
export default {
Store,
install,
}
4.3.1 挂载 $store
Vue.mixin({
beforeCreate(){
if(this.$options.store) {
Vue.prototype.$store = this.$options.store;
}
}
})
同 vue2源码学习(1) - 手写 vue-router 中挂载 $router ,这里就不再赘述。
4.3.2 state 响应式处理
根据需求分析, Store 类需要有一个响应式状态 state,这里对 options.state 响应式处理如下:
constructor(options) {
this._vm = new Vue({
data() {
return {
// $$state是不会代理到_vm上的
$$state: options.state
}
},
})
}
1、为什么要这么处理?是否可以换成其他方案?
- 方案一:在 vue2源码学习(1) - 手写 vue-router 中有提到过 Vue.util.defineReactive() 可以创建一个响应式数据;
- 方案二:在 data 中直接
return options.state;
-
constructor(options){ this.state = new Vue({ data(){ return options.state; }, }) }
-
2、方案二为什么可以实现响应式? 先来看一段大家都很熟悉的代码:
const app = new Vue({
el: '#app',
router,
store,
data(){
return {
foo: '哈哈哈',
};
},
components: { App },
template: '<App/>'
})
new Vue
给 data return
的这个对象直接会做响应式处理。
vue 在做响应式处理的时候还会做一层代理,直接把它挂载到将来创建出来的那个 vue 实例里。
所以这里的 this.state
就是一个 vue 实例,vue 实例中一定会挂着 data 中的所有 key。
因为 vue 把 data 下的 key 都代理到了 vue 实例上,所以一般会这么使用:app.foo
;而不会这么使用:app.$data.foo
。
3、方案二的问题
用户直接对 tihs.state 的数据进行修改,这会导致状态的变更将变得不可预测,我们希望用户只提交 commit。
那知道问题了,怎么解决呢?
可以将这个 vue 实例隐藏起来,不直接暴露出去。把 this.state
改为 this._vm
这时我们可以做一个存取器:
constructor(options = {}) {
this._vm = new Vue({
data: {
state: options.state
}
});
}
// 存取器
get state() {
return this._vm._data.$$state
}
set state(v) {
console.error('please use replaceState to reset state');
}
当用户想通过 this.state
去访问的时候,实际上 return 的是 this._vm
里面 data
中的值。
我也不希望用户直接通过 _vm
看到 data 中的数据,那就得把数据藏起来,不做代理,不做代理的方式:
constructor(options = {}) {
this._vm = new Vue({
data: {
state: options.state
}
});
}
// 改为
constructor(options = {}) {
this._vm = new Vue({
data: {
// $$state 是不会代理到 _vm 上的,
// 就是说在 key 前面加两个 $$ 可以让其不代理到 _vm 上
$$state: options.state
}
});
}
vue 规定在 key 前面加上
$$
或者下划线
可以让其不代理到 vue 实例上
这样便可将 data 下面的响应式数据藏起来,不让用户这么容易找到。这样可以很有限制性的去访问,访问 this.state 的时候,得到的数据是我们想让其访问到的 this._vm._data.$$state
。当然如果想接触还是可以通过 this._vm._data.$$state
或者 this._vm.$data.$$state
。
如果用户直接 set state,应该禁止并提示用户这个行为是错误的,而不是去变更状态,想改状态的话只有一种方式:
// 修改状态
commit(type, payload) {}
必须通过提供的 API 去修改,这才是可预测。
4.3.3 commit
我们平时使用 commit 是这么使用的:store.commit('add', 1)
。关于它的参数:
- type:提交 mutations 的类型
- payload:参数载荷,执行 mutations 的方法的参数
commit 的作用其实就是让 mutations 的方法执行一次,可以在 constructor 中先将 mutations 和 action 先保存下来,然后在 commit 中可以直接 this._mutations[type]
拿 mutations 里面的函数,如果这个函数不存在则直接报错。
constructor(options = {}) {
this._mutations = options.mutations
this._actions = options.actions
}
// 修改状态
commit(type, payload) {
const entry = this._mutations[type]
if(!entry) {
console.error('unknown mutation: ' + type)
}
entry(this.state, payload)
}
4.3.4 dispatch
dispatch 与 commit 如出一辙
constructor(options = {}) {
this._mutations = options.mutations
this._actions = options.actions
}
dispatch(type, payload) {
const entry = this._actions[type]
if(!entry) {
console.error('unknown action: ' + type)
}
entry(this, payload) // 第一个参数可不可以传 this ?
}
先看看 actions 中的上下文 context 是一个什么对象,context: {commit, dispatch, state, rootState}
包含了 commit 提交、dispatch 派发,这样可以在 actions 进行不同的业务逻辑组合。
上面这段代码 entry 的第一个参数能传 this 吗?因为这里的 this 正好有 commit、dispatch 等。答案当然是不可以的,因为 this 的指向问题,在 commit 的时候内部的 this 已经丢失了。
那怎么解决这个 this 的指向问题呢? 可以在 constructor 锁死 commit 和 dispatch。
constructor(options = {}) {
// 锁死 this
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}
对 myVuex.js
内容进行简单解说就到这里,欢迎大家指教~