这篇文章主要是说明如何手动实现vuex中的核心功能: state, mutations, actions
先看一下vuex的基本使用例子
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
let store = new Vuex.Store({
// 全局状态
state: {
count: 0
},
// getters属性
getters: {
doubleCount(state) {
return state.count * 2;
}
},
// 同步更新state状态
mutations: {
add(state) {
state.count ++ ;
}
},
// 异步更新state状态
actions: {
add(context) {
setTimeout(() => {
context.commit('add')
}, 1000)
}
}
})
export default store;
// main.js
import Vue from 'vue'
import store from './store'
new Vue({
router,
store, // 通过Vue构造函数将store挂载到Vue实例对象上
render: h => h(App),
}).$mount('#app')
<!-- Home.vue -->
<div>
<h2>使用store</h2>
<p @click="$store.commit('add')">count: {{$store.state.count}}</p>
<p @click="$store.dispatch('add')">async count: {{$store.state.count}}</p>
<p>double count: {{$store.getters.doubleCount}}</p>
</div>
实现过程
1. 创建一个插件:声明Vuex类,并挂载$store
// myvuex.js
// 创建一个全局变量,存储Vue构造函数引用,避免使用import方式导入Vue
let Vue;
// 我们看到在store/index中,是通过new Vuex.Store({...})来生成store实例的,所以要创建一个Store类
class Store {
// options是用来接收实例化传进来的state, getters, mutations, actions等
constructor(options) {
}
}
function install(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
// 在根实例上找到store,并将其挂载到Vue原型上的$store,便于从this.$store获取对象
if(this.$options.store) {
Vue.prototype.$store = this.$options.store
}
}
})
}
// Vuex
export default {
Store,
install
}
// 将导入的vuex文件改成自己实现的myvuex.js
// store/index.js
// import Vuex from 'vuex'
import Vuex from './myVuex/myvuex'
2. 创建响应式的state
在Vue实现数据响应式,可以有2种方法:
- 使用new Vue中的data属性
- 使用Vue.util.defineReactive(obj, property, initData)方式
class Store {
// options是用来接收实例化传进来的state, getters, mutations, actions等
constructor(options) {
// 响应化处理state
// 第1种方式
this.state = new Vue({
data: options.state
})
// 第2种方式
// Vue.util.defineReactive(this, 'state', options.state)
}
}
这时候,可以在组件中通过this.$store.state获取到状态了
3. 实现commit和dispatch方法
- 保存mutations和actions对象
- 实现commit方法,将this.state对象传到mutations的方法参数
- 实现dispatch方法,将this对象传到actions的方法参数中,方便调用store实例上的commit方法
- 在构造函数中,将commit和dispatch的this指向绑定指向store实例
class Store {
constructor(options) {
// 1. 保存mutations和actions
this._mutations = options.mutations
this._actions = options.actions
// 4. 绑定函数的this指向
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}
// 2. 实现commit方法:store.commit('add', 1)
// type: mutation的类型,也就是mutations中的方法
// payload: 载荷,也就是参数
commit(type, payload) {
const func = this._mutations[type]
console.log('func:', func);
if(func) {
func(this.state, payload)
}
}
// 3. 实现dispatch方法, store.dispatch('add', 2)
dispatch(type, payload) {
const asyncFunc = this._actions[type]
console.log('asyncFunc:', asyncFunc);
if(asyncFunc) {
asyncFunc(this, payload)
// 实际执行的是:
// add(this) {
// setTimeout(() => {
// this.commit('add', 3)
// }, 1000)
// }
// 这里,this有可能会混乱,因为在异步操作时,setTimeout的this指向的是window,所以在异步回调里执行 this.commit会报错undefined
// 所以要在构造函数中,将this.commit 和this.dispatch中this绑定到Store实例上
}
}
}
4. 实现单向数据流
我们知道用户是不能通过store.state方式修改状态的,只能通过mutation或actions来修改状态,所以我们要对this.state进行保护
所以不能直接对this.state进行响应式处理,而是另定义一个变量,然后对state属性使用存取器,get方法进行访问,set方法进行改变限制
class Store {
constructor(options) {
// this.state = new Vue({
// data: options.state
// })
// 我们不希望用户能通过store.state实例来访问或修改state属性,而是必须通过mutation或dispatch来修改
this._vm = new Vue({
data: {
// 加两个$,Vue将不做代理,也就是用户不能通过this._vm['stateProperty']的方式访问到数据
$$state: options.state
}
})
}
get state() {
return this._vm._data.$$state
}
set state(v) {
console.error("请不要直接使用this.$store.state方式直接修改数据!");
// 使用 this.$store.state = {xxx} 时就会报错提示
}
}
5. 实现getters
- 定义一个computed对象
- 遍历用户定义的getters对象,获取对应函数,并构造一个无参函数,赋予computed对象
- 将computed对象挂载到Vue实例上
- 为store实例上的getters定义只读属性,并将computed中的对象给赋予getters
class Store {
constructor(options) {
// 1. 保存用户定义的getters
this._getters = options.getters
// 实现getters计算属性
// 2. 定义一个computed对象
const computed = {}
// 3. 定义getters对象
this.getters = {}
const store = this
// 4. 遍历用户定义的getters
Object.keys(this._getters).forEach(key => {
// 5. 获取用户定义的getter
const fn = store._getters[key]
// 6. 转换成computed可以使用的无参形式
computed[key] = function() {
return fn(store.state)
}
})
// 8. 为getters定义只读属性
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
})
// 我们不希望用户能通过store.state实例来访问或修改state属性,而是必须通过mutation或dispatch来修改
this._vm = new Vue({
data: {
// 加两个$,Vue将不做代理,也就是用户不能通过this._vm['stateProperty']的方式访问到数据
$$state: options.state
},
// 7. 将computed挂载到vue实例上,实现计算属性
computed
})
}
}
完整代码
// myvuex.js
// 创建一个全局变量,存储Vue构造函数,避免使用import方式导入Vue
let Vue;
// 创建实例是new Vuex.Store({...}),所以要创建一个Store类
class Store {
// options是用来接收实例化传进来的state, getters, mutations, actions等
constructor(options) {
// 保存mutations和actions
this._mutations = options.mutations
this._actions = options.actions
this._getters = options.getters
// 实现getters计算属性
let computed = {}
this.getters = {}
const store = this
Object.keys(this._getters).forEach(key => {
// 获取用户定义的getter
const fn = store._getters[key]
// 转换成computed可以使用的无参形式
computed[key] = function() {
return fn(store.state)
}
// 为getters定义只读属性
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
})
})
// 我们不希望用户能通过store.state实例来访问或修改state属性,而是必须通过mutation或dispatch来修改
this._vm = new Vue({
data: {
// 加两个$,Vue将不做代理,也就是用户不能通过this._vm['stateProperty']的方式访问到数据
$$state: options.state
},
computed
})
// 绑定函数的this指向
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}
get state() {
return this._vm._data.$$state
}
set state(v) {
console.error("请不要直接使用this.$store.state方式直接修改数据!");
}
// 实现commit方法:store.commit('add', 1)
// type: mutation的类型,也就是mutations中的方法
// payload: 载荷,也就是参数
commit(type, payload) {
const func = this._mutations[type]
console.log('func:', func);
if(func) {
func(this.state, payload)
}
}
// 实现dispatch方法, store.dispatch('add', 2)
dispatch(type, payload) {
const asyncFunc = this._actions[type]
console.log('asyncFunc:', asyncFunc);
if(asyncFunc) {
asyncFunc(this, payload)
// 实际执行的是:
// add(this) {
// setTimeout(() => {
// this.commit('add', 3)
// }, 1000)
// }
// 这里,this有可能会混乱,因为在异步操作时,setTimeout的this指向的是window,所以在异步回调里执行 this.commit会报错undefined
// 所以要在构造函数中,将this.commit 和this.dispatch中this绑定到Store实例上
}
}
}
function install(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
// 在根实例上找到store,并将其挂载到Vue原型上的$store,便于从this.$store获取对象
if (this.$options.store) {
Vue.prototype.$store = this.$options.store;
}
},
});
}
// Vuex
export default {
Store,
install,
};
// store/index.js
import Vue from 'vue'
import Vuex from './myvuex'
Vue.use(Vuex);