基于vue2手写一个简易的Vuex__Vue.js
发布于 3 年前 作者 banyungong 1219 次浏览 来自 分享
粉丝福利 : 关注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

回到顶部