前言
vuex官方使用说明文档
vuex源码地址
- Vuex是专为Vue开发的状态管理器, 因为内部是基于vue开发
state
存放状态, getters
加工state给外界
mutations
修改state的唯一途径, actions
异步操作
modules
模块化管理, 当状态非常多时 可以分割成模块
strict
严格模式, 不合规范的写法就会报错
plugins
插件使用, 如做数据持久化, 或者修改数据打印日志(内部logger)等
state 实现的原理(描述)
- 将用户传进来的参数 进行处理 重新组装成一个结构树(内部是用递归实现的)
- 将所有的state(最外部的state和modules里的state)通过递归的方式, 组装成一个状态树, 放到一个统一的变量里
let state
- 通过
new Vue({ data: { $$state: state } })
, 组件渲染, 数据收集渲染watcher(发布订阅), 数据发生变化页面进行更新
- 描述以下方示例为主
结构树和状态树
getters 实现的原理(描述)
- 将用户传递来的所有getters, 都放在一个变量里
let warpperGetters
- 通过循环的方式 将其放到
let computed
, 并将其劫持代理Object.defineProperty
- 让用户可通过
this.$store.getter.xxxx
获取到
- 最后放在
new Vue({ computed })
- 下方是核心部分和
computed
的最终结构
store.getters = {}
const computed = {}
forEach(store.wrapperGetters, (getter, key) => {
computed[key] = getter
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
})
})
store._vm = new Vue({
data: {
$$state: state
},
computed
})
mutations 和 actions 实现的原理(描述)
- 他们也是将用户传递的参数 将所有的
mutations 和 actions
放到各自的变量中
- 如果方法名重了 将会放到一个数组里 最后通过循环调用(看下方图片)
- 用户必须通过各自的方法去使用
commit
, dispatch
(看下方代码)
- 用户可通过
this.$store.commit('xxx', params)
或者this.$store.commit('xxx/xxxx', params)
获取到
- 用户可通过
this.$store.dispatch('xxx', params)
或者this.$store.dispatch('xx/xxx', params)
获取到
- 他们的不同点就是mutations是唯一修改
state
, 并且是同步操作
- 而actions是异步操作, 如果想修改
state
内部也是提交commit
方法
commit = (mutationName, payload) => {
this.mutations[mutationName] && this.mutations[mutationName].forEach(fn => fn(payload))
}
dispatch = (actionName, payload) => {
this.actions[actionName] && this.actions[actionName].forEach(fn => fn(payload))
}
modules时的作用
- 主要做的是模块化
- 其实内部有命名空间
namespaced: true
就将state
以这个模块的key
创建个对象
- 将这个模块的
state
放进去, 最后放在状态树种, 依次进行数据模块化
- 所以用户获取值通过
this.$store.state.a.xxx
(看上方state图)
mutations 和 actions
是将方法名变成xx/xxx
(看上方mutations和actions图), 依次进行方法模块化
strict实现原理(描述)
- 是否是严格模式
true
为严格模式
- 内部主要是通过观察数据变化, 看起是否是内部方法
- 内部方法都包裹着
_withCommittting
方法
- 看下方的代码片段
store.mutations[ns + key].push((payload) => {
store._withCommittting(() => {
fn.call(store, getNewState(store, path), payload)
})
})
this._committing = false
if (store.strict) {
store._vm.$watch(() => store._vm._data.$$state, () => {
console.assert(store._committing, 'no mutate in mutation handler outside 方法不允许写在外面')
}, { deep: true, sync: true })
}
_withCommittting(fn) {
this._committing = true
fn()
this._committing = false
}
plugins实现原理(描述)
- 以logger为例
- 将插件里的方法进行订阅
subscribe
- 最后在mutations中进行发布
- 看下方代码片段
if (options.plugins) {
options.plugins.forEach(plugin => plugin(this))
}
subscribe(fn) {
this._subscribes.push(fn)
}
store.mutations[ns + key].push((payload) => {
store._withCommittting(() => {
fn.call(store, getNewState(store, path), payload)
})
store._subscribes.forEach(fn => fn({ type: ns + key, payload }, store.state))
})
为什么每个组件都能获取到$store
- 将用户创建的store放到vue实例中
- 在
Vue.use(Vuex)
时 执行install方法
- 通过
Vue.mixin
在beforeCreated执行(内部是个数组在数据劫持前依次调用 父->子->孙)
- 但只有
new Vue
放入store
中的组件 和 父级存在store
组件才有
- 通过这样 让其所有组件都注册
$store
实例
- 其他
new Vue
是没有的
- 看下方代码片段
let vm = new Vue({
store,
render: h => h(App)
}).$mount('#app')
export let Vue
function install(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
let options = this.$options
if (options.store) {
this.$store = options.store
} else {
if (this.$parent && this.$parent.$store) {
this.$store = this.$parent.$store
}
}
}
})
}
辅助函数的实现原理(描述)
mapState, mapGetters, mapMutations, mapActions
- 就是做了一层代理
- 看下方代码片段
export function mapState(stateList) {
let obj = {}
for (let i = 0; i < stateList.length; i++) {
let stateName = stateList[i]
obj[stateName] = function() {
return this.$store.state[stateName]
}
}
return obj
}
export function mapMutations(mutationList) {
let obj = {}
for (let i = 0; i < mutationList.length; i++) {
obj[mutationList[i]] = function (payload) {
this.$store.commit(mutationList[i], payload)
}
}
return obj
}
项目的目录结构
* 通过vue/cli 生成的vue2项目
├── public
│ └── index.html
├── src
│ ├── store
│ │ └── index.js
│ ├── vuex
│ │ └── module
│ │ │ ├── module-collection.js
│ │ │ └── module.js
│ │ ├── helpers.js
│ │ ├── index.js
│ │ ├── install.js
│ │ ├── store.js
│ │ └── util.js
├── App.vue
└── main.js
示例
src/store/index.js
import Vue from 'vue'
import Vuex from '@/vuex'
Vue.use(Vuex)
function logger() {
return function(store) {
let prevState = JSON.stringify(store.state)
store.subscribe((mutation, state) => {
console.log('prevState:' + prevState)
console.log('mutation:' + JSON.stringify(mutation))
console.log('currentState:' + JSON.stringify(state))
prevState = JSON.stringify(state)
})
}
}
function persists() {
return function(store) {
let localState = JSON.parse(localStorage.getItem('VUEX:STATE'))
if (localState) {
store.replaceState(localState)
}
store.subscribe((mutation, rootState) => {
localStorage.setItem('VUEX:STATE', JSON.stringify(rootState))
})
}
}
let store = new Vuex.Store({
plugins: [ logger(), persists() ],
strict: true,
state: {
name: 'zhangsan',
age: 2
},
getters: {
myAge(state) {
return state.age + 5
}
},
mutations: {
changeAge(state, payload) {
state.age += payload
}
},
actions: {
changeAge({ commit }, payload) {
setTimeout(() => {
commit('changeAge', payload);
}, 1000);
}
},
modules: {
a: {
namespaced: true,
state: {
name: 'a模块',
age: 1
},
getters: {
aAge(state) {
return state.age + 10;
}
},
mutations: {
changeAge(state, payload) {
state.age += payload
}
},
modules: {
c: {
namespaced: true,
state: {
age: 100
},
mutations: {
changeAge(state, payload) {
state.age += payload
}
}
}
}
},
b: {
state: {
name: 'b模块',
age: 2,
gender: '男'
},
getters: {
bAge(state) {
return state.age + 10;
}
},
mutations: {
changeAge(state, payload) {
state.age += payload
}
}
}
}
})
export default store
src/App.vue
<template>
<div id="app">
<h3>不是modules</h3>
<h5>state姓名年龄:</h5>
<span>{{name}} - {{age}}</span>
<hr>
<h5>getters年龄:</h5>
<span>{{this.$store.getters.myAge}}</span>
<br>
<button @click="$store.commit('changeAge',10)">更改年龄</button>
<button @click="$store.dispatch('changeAge',10)">异步年龄</button>
<button @click="$store.state.name='xxxx'">不合规范的写法$store.state.name='xxxx'会报错</button>
<hr>
<h3>是modules</h3>
<h5>a模块的state姓名年龄</h5>
<p>获取方式state.a.age</p>
<span>{{this.$store.state.a.name}} - {{this.$store.state.a.age}}</span>
<br>
<h5>a模块的getters年龄(有namespaced)</h5>
<p>获取方式getters['a/aAge']</p>
<span>{{this.$store.getters['a/aAge']}}</span>
<br>
<button @click="$store.commit('a/changeAge',10)">a模块更改年龄commit('a/changeAge',10)</button>
<hr>
<h5>b模块的state姓名年龄性别(无namespaced)</h5>
<p>获取方式state.b.age</p>
<span>{{this.$store.state.b.name}} - {{this.$store.state.b.age}} - {{this.$store.state.b.gender}}</span>
<br>
<button @click="$store.commit('changeAge',10)">b模块更改年龄commit('changeAge',10), 获取方式覆盖, 并且外围的state也会改变</button>
<br>
<h5>a/c模块的state年龄(有namespaced)</h5>
<p>获取方式state.a.c.age</p>
<span>{{this.$store.state.a.c.age}}</span>
<br>
<button @click="$store.commit('a/c/changeAge',10)">a/c模块更改年龄commit('a/c/changeAge',10)</button>
<hr>
<h3>用户注册模块</h3>
<span>
{{this.$store.state.rModule && this.$store.state.rModule.name}} -
{{this.$store.state.rModule && this.$store.state.rModule.number}} -
{{this.$store.getters.rGetterNumber && this.$store.getters.rGetterNumber}}
</span>
<br>
<button @click="registerModule">手动注册模块</button>
</div>
</template>
<script>
import store from './store'
import { mapState } from './vuex/index'
export default {
name: 'app',
computed: {
...mapState(['name', 'age'])
},
methods: {
registerModule() {
store.registerModule('rModule', {
state: {
name: 'rModule',
number: 5,
},
getters: {
rGetterNumber(state) {
return state.number+5
}
}
})
},
},
}
</script>
<style>
#app span {
color: blue;
}
button {
height: 35px;
line-height: 35px;
padding:0 10px;
margin-top: 10px;
margin-right: 10px;
color: #fff;
background-color: #000;
outline: none;
border: 0;
}
</style>
正题
src/vuex/index.js
import install from './install'
import Store from './store'
import { mapState, mapGetters, mapMutations, mapActions } from './helpers'
export {
Store,
install,
mapState,
mapMutations,
mapGetters,
mapActions
}
export default {
install,
Store,
mapState,
mapGetters,
mapMutations,
mapActions
}
src/vuex/install.js
export let Vue
function install(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
let options = this.$options
if (options.store) {
this.$store = options.store
} else {
if (this.$parent && this.$parent.$store) {
this.$store = this.$parent.$store
}
}
}
})
}
export default install
src/vuex/store.js
import { Vue } from './install'
import ModuleCollection from './module/module-collection'
import { forEach } from './util'
function getNewState(store, path) {
return path.reduce((memo, current) => {
return memo[current]
}, store.state)
}
function installModule(store, rootState, path, module) {
let ns = store._modules.getNamespace(path)
if (path.length > 0) {
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo[current]
}, rootState)
store._withCommittting(() => {
Vue.set(parent, path[path.length - 1], module.state)
})
}
module.forEachGetter((fn, key) => {
store.wrapperGetters[ns + key] = function() {
return fn.call(store, getNewState(store, path))
}
})
module.forEachMutation((fn, key) => {
store.mutations[ns + key] = store.mutations[ns + key] || []
store.mutations[ns + key].push((payload) => {
store._withCommittting(() => {
fn.call(store, getNewState(store, path), payload)
})
store._subscribes.forEach(fn => fn({ type: ns + key, payload }, store.state))
})
})
module.forEachAction((fn, key) => {
store.actions[ns + key] = store.actions[ns + key] || []
store.actions[ns + key].push((payload) => {
return fn.call(store, store, payload)
})
})
module.forEachChildren((child, key) => {
installModule(store, rootState, path.concat(key), child)
})
}
function resetVM(store, state) {
let oldVm = store._vm
store.getters = {}
const computed = {}
forEach(store.wrapperGetters, (getter, key) => {
computed[key] = getter
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
})
})
store._vm = new Vue({
data: {
$$state: state
},
computed
})
if (store.strict) {
store._vm.$watch(() => store._vm._data.$$state, () => {
console.assert(store._committing, 'no mutate in mutation handler outside 方法不允许写在外面')
}, { deep: true, sync: true })
}
if (oldVm) {
Vue.nextTick(() => oldVm.$destroy())
}
}
class Store {
constructor(options) {
this._modules = new ModuleCollection(options)
this.wrapperGetters = {}
this.mutations = {}
this.actions = {}
this._subscribes = []
this._committing = false
this.strict = options.strict
let state = options.state
installModule(this, state, [], this._modules.root)
resetVM(this, state)
if (options.plugins) {
options.plugins.forEach(plugin => plugin(this))
}
}
_withCommittting(fn) {
this._committing = true
fn()
this._committing = false
}
subscribe(fn) {
this._subscribes.push(fn)
}
replaceState(newState) {
this._withCommittting(() => {
this._vm._data.$$state = newState
})
}
get state() {
return this._vm._data.$$state
}
commit = (mutationName, payload) => {
this.mutations[mutationName] && this.mutations[mutationName].forEach(fn => fn(payload))
}
dispatch = (actionName, payload) => {
this.actions[actionName] && this.actions[actionName].forEach(fn => fn(payload))
}
registerModule(path, module) {
if (typeof path == 'string') path = [path]
this._modules.register(path, module)
installModule(this, this.state, path, module.newModule)
resetVM(this, this.state)
}
}
export default Store
src/vuex/module/module-collection.js
import { forEach } from '../util'
import Module from './module'
class ModuleCollection {
constructor(options) {
this.root = null
this.register([], options)
}
getNamespace(path) {
let root = this.root
let ns = path.reduce((ns,key) => {
let module = root.getChild(key)
root = module;
return module.namespaced ? ns + key + '/' : ns
}, '')
return ns
}
register(path, rawModule) {
let newModule = new Module(rawModule)
rawModule.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 (rawModule.modules) {
forEach(rawModule.modules,(module,key) => {
this.register(path.concat(key), module)
})
}
}
}
export default ModuleCollection
src/vuex/module/module.js
import { forEach } from "../util"
class Module {
constructor(rawModule) {
this._raw = rawModule
this._children = {}
this.state = rawModule.state
}
getChild(childName) {
return this._children[childName]
}
addChild(childName, module) {
this._children[childName] = module
}
forEachGetter(cb) {
this._raw.getters && forEach(this._raw.getters, cb)
}
forEachMutation(cb) {
this._raw.mutations && forEach(this._raw.mutations, cb)
}
forEachAction(cb) {
this._raw.actions && forEach(this._raw.actions, cb)
}
forEachChildren(cb) {
this._children && forEach(this._children, cb)
}
get namespaced() {
return !!this._raw.namespaced
}
}
export default Module
src/vuex/util.js
export const forEach = (obj, fn) => {
Object.keys(obj).forEach(key => {
fn(obj[key], key)
})
}
src/vuex/helpers.js
export function mapState(stateList) {
let obj = {}
for (let i = 0; i < stateList.length; i++) {
let stateName = stateList[i]
obj[stateName] = function() {
return this.$store.state[stateName]
}
}
return obj
}
export function mapGetters(gettersList) {
let obj = {}
for (let i = 0; i < gettersList.length; i++) {
let getterName = gettersList[i]
obj[getterName] = function() {
return this.$store.getters[getterName]
}
}
return obj
}
export function mapMutations(mutationList) {
let obj = {}
for (let i = 0; i < mutationList.length; i++) {
obj[mutationList[i]] = function (payload) {
this.$store.commit(mutationList[i], payload)
}
}
return obj
}
export function mapActions(actionList) {
let obj = {}
for (let i = 0; i < actionList.length; i++) {
obj[actionList[i]] = function (payload) {
this.$store.dispatch(actionList[i], payload)
}
}
return obj
}
完