结合使用方式,重温核心源码,你也可以实现一个简易的vuex。
- 我们是如何使用vuex的? 首先,在main.js引入配置好的store,
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
store,
}).$mount('#app')
App.vue 中使用state中的值,
<template>
<div id="app">
my age {{age}}
<br>
your age {{getAge}}
<br>
<button @click="$store.state.age += 100">直接修改状态</button>
<button @click="changeAge(2)">同步更新状态</button>
<br>
<button @click="$store.dispatch('changeAge', 3)">异步更新状态</button>
</div>
</template>
import {mapState, mapGetters, mapMutations, mapAtions} from './vuex'
export default {
name: 'App',
computed: {
...mapState(['age']),
...mapGetters(['getAge'])
},
created() {
console.log('app=', this.$store)
},
methods: {
...mapMutations(['changeAge']),
// ...mapAtions(['changeAge'])
// changeAge(payload) {
// this.$store.commit('changeAge', payload)
// }
}
}
然后编写自己的store.js, vuex是和Vue强绑定的,利用Vue的插件机制,在Vue中先安装vuex;
import Vue from 'vue'
// import Vuex from 'vuex'
import Vuex from '../vuex'
Vue.use(Vuex)
// 内部会创建一个vue实例,用于跨组件通信
const store = new Vuex.Store({
// 组件状态 ==> new Vue的data
state: {
//..
},
// 获取计算属性,当依赖的值变化时 会重新执行
getters: {
//..
},
// new Vue中的method,唯一可以改状态的方法
mutations: {
//..
},
// 如果改变的异步的,需要在actions中发起请求,
actions: {
//..
},
modules: {
//..
}
})
export default store
2.实现vuex
首先从入口文件开始,vuex/index.js作为入口文件,作用就是整合;
import { Store, install } from './store'
import {mapState, mapGetters, mapMutations, mapAtions} from './helper'
export default {
Store,
install,
mapState,
mapGetters,
mapMutations,
mapAtions
}
然后来到核心文件vuex/store.js,先看主体结构,
import ModuleCollections from './module/module-collections'
let Vue;
class Store {
this._modules = new ModuleCollections(options)
//...
installModule(this, state, [], this._modules.root)
//...
resetStoreVm(this, state)
//...
}
const install = (_Vue) => {
Vue = _Vue;
applyMixin(Vue);
}
export {
Store,
install
}
vuex的store是如何挂载注入到组件中呢?
利用vue的插件机制,使用Vue.use(vuex)时,会调用vuex的install方法,装载vuex;applyMixin方法使用vue混入机制,在vue的生命周期beforeCreate钩子函数前混入vuexInit方法;
组件的创建过程是 先父后子,最后是给每个组件都加了$store这个属性,指向同一个属性,所以最后状态是公用的;
和vue-router不一样(vue-router 是把属性定义到根实例上,所有组件都能找这个根,通过根实例获取这个属性)。
const applyMixin = (Vue) => {
Vue.mixin({
beforeCreate: VuexInit
})
}
function VuexInit() {
// 获取当前Vue实例的选项
const options = this.$options
// 只有根实例上才有store属性
if(options.store) { // 根组件
this.$store = options.store
} else if(options.parent && options.parent.$store){ // 子组件 有parent时
this.$store = options.parent.$store
}
}
再看初始化时,
1)我们先实例化ModuleCollections,格式化用户传入的参数成树形结构,更直观更好操作;
看下代码vuex/module/module-collections.js,通过递归注册模块,收集模块转换成一棵树;
import {forEach} from '../utils.js'
import Module from './module'
export default class ModuleCollection {
constructor(options) {
// 注册模块
this.register([], options)
}
register(path, rootModule) {
let newModule = new Module(rootModule)
rootModule.newModule = newModule // 给当前要注册的模块上 做一个映射
if(path.length == 0) { // 根模块
this.root = newModule
} else {
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo.getChild(current)
}, this.root)
parent.addChild(path[path.length - 1], newModule)
}
if(rootModule.modules) { // 如果有modules 说明有子模块
forEach(rootModule.modules, (module, moduleName) => {
this.register([...path, moduleName], module)
})
}
}
// 获取命名空间
getNamespace(path) {
let root = this.root // [b,c]
return path.reduce((namespace, key) => {
root = root.getChild(key)
// namespaced 如果有值,则不停的拼接
return namespace + (root.namespaced ? key + '/' : '')
}, '')
}
}
Module类定义了模块的一些属性和方法,在vuex/module/module.js里;
import {forEach} from '../utils.js'
export default class Module {
constructor(rootModule) {
this._rawModule = rootModule;
this._children = {};
this.state = rootModule.state;
}
get namespaced() {
return this._rawModule.namespaced
}
getChild(key) {
return this._children[key]
}
addChild(key, module) {
this._children[key] = module;
}
forEachMutation(fn) {
if(this._rawModule.mutations) {
forEach(this._rawModule.mutations, fn)
}
}
forEachAction(fn) {
if(this._rawModule.actions) {
forEach(this._rawModule.actions, fn)
}
}
forEachGetter(fn) {
if(this._rawModule.getters) {
forEach(this._rawModule.getters, fn)
}
}
forEachChild(fn) {
forEach(this._children, fn)
}
}
2)执行 installModule 方法,注册模块,如果是子模块,需要将子模块的状态定义到根模块;
function installModule(store, rootState, path, module) {
// 注册事件时,需要注册到对应的命名空间,path就是所有的路径,根据path算出一个空间里
let namespace = store._modules.getNamespace(path)
console.log('namepase', namespace)
// 如果是子模块 需要将子模块的状态定义到根模块
if(path.length > 0) {
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo[current]
}, rootState)
store._withCommitting(() => {
// Vue.set区分是否是响应式数据
Vue.set(parent, path[path.length - 1], module.state)
})
}
module.forEachMutation((mutation, type) => {
store._mutations[namespace + type] = store._mutations[namespace + type] || []
store._mutations[namespace + type].push((payload) => {
// mutation.call(store, module.state, payload)
// 只有通过mutation更改状态,断言才能通过
store._withCommitting(() => {
mutation.call(store, getState(store, path), payload)
})
// 执行订阅的事件
store._subscribers.forEach(sub => sub({mutation, type}, store.state))
})
})
module.forEachAction((action, type) => {
store._actions[namespace + type] = store._actions[namespace + type] || []
store._actions[namespace + type].push((payload) => {
action.call(store, store, payload)
})
})
module.forEachGetter((getter, type) => {
// 如果getters 重名会覆盖,所有模块的getter都会定义到根模块上
store._wrappedGetters[namespace + type] = function() {
return getter(getState(store, path))
}
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child)
})
}
调用 forEachMutation 方法,将用户传入的mutations转换,存放到存放所有模块的mutations的_mutations对象中,键是'namespace + type',以此区分不同模块下的同名mutation,值是mutation对应的方法的数组,在用户执行commit时,依次执行对应的方法;
commit = (type, payload) => {
this._mutations[type].forEach(fn => fn(payload))
}
同理,执行forEachAction,将用户传入的actions转换,存放到存放所有模块的actions的_actions对象中。
不同之处在于:mutation被包裹了一层_withCommitting,只有通过mutation更改状态,断言才能通过;
// mutations
store._mutations[namespace + type].push((payload) => {
store._withCommitting(() => {
mutation.call(store, getState(store, path), payload)
})
})
//actions
store._actions[namespace + type].push((payload) => {
action.call(store, store, payload)
})
// 切片,判断是否是mutation修改状态
_withCommitting(fn) {
let committing = this._committing
this._committing = true
fn()
this._committing = committing
}
//在resetStoreVm里
store._vm.$watch(() => store._vm._data.$$state, () => {
console.assert(store._committing, '在mutation之外更改了状态')
}, {deep: true, sync: true})
3)执行resetStoreVm(this, state),将用户传入的数据定义在vue的实例上,产生一个单独的vue实例进行通信,这个就是vuex核心。
function resetStoreVm(store, state) {
const wrappedGetters = store._wrappedGetters
let oldVm = store._vm
let computed = {} // 通过computed实现缓存效果
store.getters = {}
// 让getters 定义在store上
forEach(wrappedGetters, (fn, key) => {
computed[key] = function() {
return fn()
}
// 重写 get 方法
// store.getters.xx 其实是访问了store._vm[xx],其中添加 computed 属性
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
})
})
// 创建Vue实例来保存state,同时让state变成响应式
// store._vm._data.$$state = store.state
store._vm = new Vue({
data: {
$$state: state
},
computed
})
if(store.strict) {
// 只有状态变化 会立即执行
store._vm.$watch(() => store._vm._data.$$state, () => {
console.assert(store._committing, '在mutation之外更改了状态')
}, {deep: true, sync: true})
}
if(oldVm) {
Vue.nextTick(() => {
oldVm.$destroy()
})
}
}
最后对照vuex/store.js完整代码看一下:
import ModuleCollections from './module/module-collections'
let Vue;
function installModule(store, rootState, path, module) {
// 注册事件时,需要注册到对应的命名空间,path就是所有的路径,根据path算出一个空间里
let namespace = store._modules.getNamespace(path)
console.log('namepase', namespace)
// 如果是子模块 需要将子模块的状态定义到根模块
if(path.length > 0) {
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo[current]
}, rootState)
store._withCommitting(() => {
// Vue.set区分是否是响应式数据
Vue.set(parent, path[path.length - 1], module.state)
})
}
module.forEachMutation((mutation, type) => {
store._mutations[namespace + type] = store._mutations[namespace + type] || []
store._mutations[namespace + type].push((payload) => {
// mutation.call(store, module.state, payload)
// 只有通过mutation更改状态,断言才能通过
store._withCommitting(() => {
mutation.call(store, getState(store, path), payload)
})
// 执行订阅的事件
store._subscribers.forEach(sub => sub({mutation, type}, store.state))
})
})
module.forEachAction((action, type) => {
store._actions[namespace + type] = store._actions[namespace + type] || []
store._actions[namespace + type].push((payload) => {
action.call(store, store, payload)
})
})
module.forEachGetter((getter, type) => {
// 如果getters 重名会覆盖,所有模块的getter都会定义到根模块上
store._wrappedGetters[namespace + type] = function() {
return getter(getState(store, path))
}
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child)
})
}
function resetStoreVm(store, state) {
const wrappedGetters = store._wrappedGetters
let oldVm = store._vm
let computed = {} // 通过computed实现缓存效果
store.getters = {}
// 让getters 定义在store上
forEach(wrappedGetters, (fn, key) => {
computed[key] = function() {
return fn()
}
// 重写 get 方法
// store.getters.xx 其实是访问了store._vm[xx],其中添加 computed 属性
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
})
})
// 将用户传入的数据定义在vue的实例上 (这个就是vuex核心)产生一个单独的vue实例进行通信
// 创建Vue实例来保存state,同时让state变成响应式
// store._vm._data.$$state = store.state
store._vm = new Vue({
data: {
$$state: state
},
computed
})
if(store.strict) {
// 只有状态变化 会立即执行
store._vm.$watch(() => store._vm._data.$$state, () => {
console.assert(store._committing, '在mutation之外更改了状态')
}, {deep: true, sync: true})
}
if(oldVm) {
Vue.nextTick(() => {
oldVm.$destroy()
})
}
}
// 通过路径 获得最新的state
function getState(store, path) {
return path.reduce((newState, current) => {
return newState[current]
}, store.state)
}
const applyMixin = (Vue) => {
Vue.mixin({
beforeCreate: VuexInit
})
}
// 组件的创建过程是 先父后子,最后是给每个组件都加了$store这个属性,指向同一个属性,所以最后状态是公用的
// 和vue-router不一样(vue-router 是把属性定义到根实例上,所有组件都能找这个根,通过根实例获取这个属性)
function VuexInit() {
// 获取当前Vue实例的选项
const options = this.$options
// 只有根实例上才有store属性
if(options.store) { // 根组件
this.$store = options.store
} else if(options.parent && options.parent.$store){ // 子组件 有parent时
this.$store = options.parent.$store
}
}
class Store {
constructor(options) {
// 格式化用户传入的参数,成树形结构,更直观 更好操作
// 收集模块转换成一棵树
this._modules = new ModuleCollections(options)
console.log(this._modules)
// 安装模块 将模块上的属性 定义到store中
let state = this._modules.root.state
this._mutations = {} // 存放所有模块的
this._actions = {} // 存放所有模块的
this._wrappedGetters = {} // 存放所有模块的
this._subscribers = []
this.strict = options.strict // 严格模式
this._committing = false // 同步的watcher
// console.log('this._mutations', this._mutations);
// console.log('this._actions', this._actions);
// console.log('this._wrappedGetters', this._wrappedGetters);
installModule(this, state, [], this._modules.root)
// 将状态放到vue实例中
resetStoreVm(this, state)
// 插件的实现
options.plugins.forEach(plugin => plugin(this))
}
// 切片,判断是否是mutation修改状态
_withCommitting(fn) {
let committing = this._committing
this._committing = true
fn()
this._committing = committing
}
subscribe(fn) {
// 先存
this._subscribers.push(fn)
// 状态变化时(调用commit) 执行
}
replaceState(newState) { // 用最新的状态替换
this._withCommitting(() => {
this._vm._data.$$state = newState
})
}
commit = (type, payload) => {
this._mutations[type].forEach(fn => fn(payload))
}
dispatch = (type, payload) => {
this._actions[type].forEach(fn => fn(payload))
}
// 类的属性访问器,用户去实例上获取state属性时,执行此方法
get state() {
return this._vm._data.$$state
}
registerModule(path, rawModule) {
if(typeof type === 'string') path = [path]
// 模块注册
this._modules.register(path, rawModule)
// 模块安装 动态将状态新增上去
installModule(this, this.state, path, rawModule.newModule)
// 重新定义getters
resetStoreVm(this, this.state)
}
}
const install = (_Vue) => {
Vue = _Vue;
applyMixin(Vue);
}
export {
Store,
install
}
- 总结 1)vuex的state状态是响应式,是借助vue的data是响应式,将state存入vue实例组件的data中;
2)Vuex的getters则是借助vue的计算属性computed实现数据实时监听;
3)mutatons类似new Vue中的method,是唯一可以改状态的方法;
4)如果改变的异步的,需要在actions中发起请求;
5)默认模块没有作用域,状态不要和模块重名,如果增加namespaced,会将这个模块的属性都封装到这个作用域下;
6)默认会找当前模块上是否有namespace, 并且会将父级的一同算上,计算成命名空间;