基于vue2手写一个简易的Vuex__Vue.js
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利
主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green
贡献主题:https://github.com/xitu/juejin-markdown-themes
theme: smartblue highlight: atom-one-dark
前言
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
方法 - 看下方的代码片段
// 内部motations方法都包裹着_withCommittting
store.mutations[ns + key].push((payload) => {
// 看这里
store._withCommittting(() => {
fn.call(store, getNewState(store, path), payload)
})
})
// 默认为false
this._committing = false
/** 严格模式要监控状态 通过同步的watcehr 深度观察 */
/** store._committing 为 false 就是不合规范的写法 直接断言报错 */
if (store.strict) {
store._vm.$watch(() => store._vm._data.$$state, () => {
// sync = true 将watcher改为同步 状态变化会立即执行 不是异步watcher
console.assert(store._committing, 'no mutate in mutation handler outside 方法不允许写在外面')
// 内部会遍历所有的属性
}, { deep: true, sync: true })
}
/**
* @description 如果用户开启了严格模式strict=true 如果写法不合规定 直接报错
* @description 主要是在内部做了个叠片 修改数据时 包裹了此方法
* @description 如在外界直接通过$store.state.xxx = 'xxx' 没有包裹此方法this._committing = false
* @description this._committing 始终为 false 直接报错
*/
_withCommittting(fn) {
this._committing = true
fn()
this._committing = false
}
plugins实现原理(描述)
- 以logger为例
- 将插件里的方法进行订阅
subscribe
- 最后在mutations中进行发布
- 看下方代码片段
// 是否使用了插件
if (options.plugins) {
options.plugins.forEach(plugin => plugin(this))
}
/**
* @description 订阅
*/
subscribe(fn) {
this._subscribes.push(fn)
}
// 发布
store.mutations[ns + key].push((payload) => {
store._withCommittting(() => {
fn.call(store, getNewState(store, path), payload) // 先调用mutation 在执行subscirbe
})
// 当数据发生变化时 发布 logger
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
是没有的 - 看下方代码片段
// main.js
let vm = new Vue({
store, // 此store的目的是让所有组件都能访问到store对象
render: h => h(App)
}).$mount('#app')
// 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
}
}
}
})
}
辅助函数的实现原理(描述)
mapState, mapGetters, mapMutations, mapActions
- 就是做了一层代理
- 看下方代码片段
// helper.js
/**
* @description 辅助函数 mapState
*/
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
}
/**
* @description 辅助函数 mapMutations
*/
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'
// vuex内部的logger
// import logger from 'vuex/dist/logger.js'
Vue.use(Vuex)
/** 插件方法(简单处理) */
/**
* @description 数据的每次更新 都打日志
* @description 状态变化都是通过mutation的 使用commit()提交 其他无效
*/
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)
})
}
}
/**
* @description 页面刷新 数据不重置 做持久化
* @description 这里放到localhost中
*/
function persists() {
return function(store) {
let localState = JSON.parse(localStorage.getItem('VUEX:STATE'))
// 替换老数据
if (localState) {
store.replaceState(localState)
}
// 每次数据变化都存储在localStorage中
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);
}
},
/**
* @description 子模块的名字不能和父模块中的state重名
* @description namespaced(重点) 能解决子模块和父模块的命名冲突文件 增加独立的命名空间
* @description 如果没有namespaced 默认getters都会被定义到父模块上
* @description 如果没有namespaced mutations会被合并在一起 最终一起调用
* @description 有了命名空间就没有这个问题了
*/
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'
/**
* @description 得到最新的state(数据劫持过得)
* @description 例如persists刷新页面 获取当前最新的值
*/
function getNewState(store, path) {
return path.reduce((memo, current) => {
return memo[current]
}, store.state)
}
/**
* 安装模块 wrapperGetters actions mutations
*/
function installModule(store, rootState, path, module) {
// a/b/c
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) // 先调用mutation 在执行subscirbe
})
// 当数据发生变化时 发布 logger
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)
})
}
/**
* @description 重新注册vm
*/
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
})
/** 严格模式要监控状态 通过同步的watcehr 深度观察 */
/** store._committing 为 false 就是不合规范的写法 直接断言报错 */
if (store.strict) {
store._vm.$watch(() => store._vm._data.$$state, () => {
// sync = true 将watcher改为同步 状态变化会立即执行 不是异步watcher
console.assert(store._committing, 'no mutate in mutation handler outside 方法不允许写在外面')
// 内部会遍历所有的属性
}, { deep: true, sync: true })
}
// 重新创建实例后,需要将老的实例卸载掉
if (oldVm) {
Vue.nextTick(() => oldVm.$destroy())
}
}
/**
* @description 对用户的模块进行整合
*/
class Store {
constructor(options) {
// 对用户的参数进行格式化操作
this._modules = new ModuleCollection(options)
this.wrapperGetters = {}
// 将模块中的所有的getters,mutations,actions进行收集
this.mutations = {}
this.actions = {}
this._subscribes = []
// 默认不是在mutation中更改的
this._committing = false
// 是否有严格模式
this.strict = options.strict
// 无namespaced的 getters都放在根上
// mutations和actions 会被合并数组
let state = options.state
installModule(this, state, [], this._modules.root)
resetVM(this, state)
// 是否使用了插件
if (options.plugins) {
options.plugins.forEach(plugin => plugin(this))
}
}
/**
* @description 如果用户开启了严格模式strict=true 如果写法不合规定 直接报错
* @description 主要是在内部做了个叠片 修改数据时 包裹了此方法
* @description 如在外界直接通过$store.state.xxx = 'xxx' 没有包裹此方法this._committing = false
* @description this._committing 始终为 false 直接报错
*/
_withCommittting(fn) {
this._committing = true
fn()
this._committing = false
}
/**
* @description 订阅
*/
subscribe(fn) {
this._subscribes.push(fn)
}
/**
* @description 替换状态
*/
replaceState(newState) {
this._withCommittting(() => {
this._vm._data.$$state = newState
})
}
/**
* @description 获取当前的状态
*/
get state() {
return this._vm._data.$$state
}
/**
* @description $store.commit dispatch 发布
*/
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))
}
/**
* @description 用户注册模块
*/
registerModule(path, module) {
if (typeof path == 'string') path = [path]
// module 是用户直接写的
this._modules.register(path, module)
// 将用户的module 重新安装
installModule(this, this.state, path, module.newModule)
// vuex内部重新注册的话 会重新生成实例
// 虽然重新安装了 只解决了状态的问题 但是computed就丢失了
// 销毁重来
resetVM(this, this.state)
}
}
export default Store
src/vuex/module/module-collection.js
import { forEach } from '../util'
import Module from './module'
/** * * * * * * * * * * * * * * * * * *
* @description 最终收集的结果
* this.root = {
* _raw: 用户定义的模块,
* state: 当前模块自己的状态,
* _children: { 孩子列表
* a: {
* _raw: 用户定义的模块,
* state: 当前模块自己的状态,
* _children: { 孩子列表
* e: {}
* }
* },
* c: {
*
* }
* }
* }
** * * * * * * * * * * * * * * * * * */
class ModuleCollection {
constructor(options) {
this.root = null
// 核心方法
this.register([], options)
}
/**
* @description 如果module有namespaced 将模块名进行拼接
* @description [a,b,c] -> 'a/b/c'
*/
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
}
/**
* @description 注册模块 将模块进行封装
*/
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)
/** 根据当前注册的key ,将他注册到对应的模块的儿子处 */
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
}
/**
* @description 得到子元素模块
*/
getChild(childName) {
return this._children[childName]
}
/**
* @description 添加子元素模块
*/
addChild(childName, module) {
this._children[childName] = module
}
/**
* @description 循环getters mutations actions 用于收集
*/
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)
}
/**
* @description 循环_children 用于递归 收集getters mutations actions
*/
forEachChildren(cb) {
this._children && forEach(this._children, cb)
}
/**
* @description 自己是否写了namespaced
*/
get namespaced() {
return !!this._raw.namespaced
}
}
export default Module
src/vuex/util.js
/**
* @description 循环对象 并执法方法
*/
export const forEach = (obj, fn) => {
Object.keys(obj).forEach(key => {
fn(obj[key], key)
})
}
src/vuex/helpers.js
/**
* @description 辅助函数 mapState
*/
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
}
/**
* @description 辅助函数 mapGetters
*/
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
}
/**
* @description 辅助函数 mapMutations
*/
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
}
/**
* @description 辅助函数 mapActions
*/
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
}
完
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 小桂summer 原文链接:https://juejin.im/post/6919762899858620424