vuex mapstate_学习 vuex 源码整体架构,打造属于自己的状态管理库

vuex mapstate_学习 vuex 源码整体架构,打造属于自己的状态管理库:https://blog.csdn.net/weixin_39899244/article/details/110469658?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-0.pc_relevant_default&spm=1001.2101.3001.4242.1&utm_relevant_index=3

 

前言

你好,我是若川。这是学习源码整体架构第五篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。

学习源码整体架构系列文章如下:

1.若川:学习 jQuery 源码整体架构,打造属于自己的 js 类库 
2.若川:学习underscore源码整体架构,打造属于自己的函数式编程类库 
3.若川:学习 lodash 源码整体架构,打造属于自己的函数式编程类库 
4.若川:学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK 
5.若川:学习 vuex 源码整体架构,打造属于自己的状态管理库 
6.若川:学习 axios 源码整体架构,打造属于自己的请求库 
7.若川:学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

感兴趣的读者可以点击阅读。 其他源码计划中的有:expressvue-routerredux、 react-redux等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。

源码类文章,一般阅读量不高。已经有能力看懂的,自己就看了。不想看,不敢看的就不会去看源码。
所以我的文章,尽量写得让想看源码又不知道怎么看的读者能看懂。

导读
文章比较详细的介绍了vuexvue源码调试方法和 Vuex 原理。并且详细介绍了 Vuex.use 安装和 new Vuex.Store 初始化、Vuex.Store 的全部API(如dispatchcommit等)的实现和辅助函数 mapStatemapGetters、 mapActionsmapMutations createNamespacedHelpers

chrome 浏览器调试 vuex 源码方法

Vue文档:在 VS Code 中调试 Vue 项目
从上文中同理可得调试 vuex 方法,这里详细说下,便于帮助到可能不知道如何调试源码的读者。
可以把笔者的这个 vuex-analysis 源码分析仓库fork一份或者直接克隆下来, git clone https://github.com/lxchuan12/vuex-analysis.git

其中文件夹 vuex,是克隆官方的 vuex仓库 dev分支。 
截至目前(2019年11月),版本是 v3.1.2,最后一次 commit是 ba2ff3a3, 2019-11-11 11:51 Ben Hutton。 
包含笔者的注释,便于理解。

克隆完成后, 在vuex/examples/webpack.config.js 中添加devtool配置。

  1.  
    // 新增devtool配置,便于调试
  2.  
    devtool: 'source-map',
  3.  
    output: {}
  4.  
    git clone https://github.com/lxchuan12/vuex-analysis.git
  5.  
    cd vuex
  6.  
    npm i
  7.  
    npm run dev
打开 http://localhost:8080/ 
点击你想打开的例子,例如:Shopping Cart => http://localhost:8080/shopping-cart/ 
打开控制面板 source 在左侧找到 webapck// . src 目录 store 文件 根据自己需求断点调试即可。

本文主要就是通过Shopping Cart,(路径vuex/examples/shopping-cart)例子调试代码的。

顺便提一下调试 vue 源码(v2.6.10)的方法

git clone https://github.com/vuejs/vue.git

克隆下来后将package.json 文件中的script dev命令后面添加这个 --sourcemap

  1.  
    {
  2.  
    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap"
  3.  
    }
  4.  
    git clone https://github.com/vuejs/vue.git
  5.  
    cd vue
  6.  
    npm i
  7.  
    # 在 dist/vue.js 最后一行追加一行 //# sourceMappingURL=vue.js.map
  8.  
    npm run dev
  9.  
    # 新终端窗口
  10.  
    # 根目录下 全局安装http-server(一行命令启动服务的工具)
  11.  
    npm i -g http-server
  12.  
    hs -p 8100
  13.  
     
  14.  
    # 在examples 文件夹中把引用的vuejs的index.html 文件 vue.min.js 改为 vue.js
  15.  
    # 或者把dist文件夹的 vue.min.js ,替换成npm run dev编译后的dist/vue.js
  16.  
     
  17.  
    # 浏览器打开 open http://localhost:8100/examples/
  18.  
     
  19.  
    # 打开控制面板 source 在左侧找到 src 目录 即vue.js源码文件 根据自己需求断点调试即可。

本小节大篇幅介绍调试方法。是因为真的很重要。会调试代码,看源码就比较简单了。关注主线调试代码,很容易看懂。强烈建议克隆笔者的这个仓库,自己调试代码,对着注释看,不调试代码,只看文章不容易吸收消化
笔者也看了文章末尾笔者推荐阅读的文章,但还是需要自己看源代码,才知道这些文章哪里写到了,哪里没有细写。

正文开始~

vuex 原理

简单说明下 vuex 原理

  1.  
    <template>
  2.  
    <div>
  3.  
    count {{$store.state.count}}
  4.  
    </div>
  5.  
    </template>

每个组件(也就是Vue实例)在beforeCreate的生命周期中都混入(Vue.mixin)同一个Store实例 作为属性 $store, 也就是为啥可以通过 this.$store.dispatch 等调用方法的原因。

最后显示在模板里的 $store.state.count 源码是这样的。

  1.  
    class Store{
  2.  
    get state () {
  3.  
    return this._vm._data.$$state
  4.  
    }
  5.  
    }

其实就是: vm.$store._vm._data.$$state.count 其中vm.$store._vm._data.$$state 是 响应式的。 怎么实现响应式的?其实就是new Vue()

  1.  
    function resetStoreVM (store, state, hot) {
  2.  
    // 省略若干代码
  3.  
    store._vm = new Vue({
  4.  
    data: {
  5.  
    $$state: state
  6.  
    },
  7.  
    computed
  8.  
    })
  9.  
    // 省略若干代码
  10.  
    }

这里的 state 就是 用户定义的 state。 这里的 computed 就是处理后的用户定义的 getters。 而 class Store上的一些函数(API)主要都是围绕修改vm.$store._vm._data.$$statecomputed(getter)服务的。

Vue.use 安装

笔者画了一张图表示下Vuex对象,是Vue的一个插件。

 

d52f459efbaece33b8091a3aca82aa35.png
看到这里,恭喜你已经了解了 Vuex原理。文章比较长,如果暂时不想关注源码细节,可以克隆一下本仓库代码 git clone https://github.com/lxchuan12/vuex-analysis.git,后续调试代码,点赞收藏到时想看了再看。

文档 Vue.use Vue.use(Vuex)

参数: {Object | Function} plugin 用法: 
安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。 install 方法调用时,会将 Vue 作为参数传入。 
该方法需要在调用 new Vue() 之前被调用。 
当 install 方法被同一个插件多次调用,插件将只会被安装一次。

根据断点调试,来看下Vue.use的源码。

  1.  
    function initUse (Vue) {
  2.  
    Vue.use = function (plugin) {
  3.  
    var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
  4.  
    // 如果已经存在,则直接返回this也就是Vue
  5.  
    if (installedPlugins.indexOf(plugin) > -1) {
  6.  
    return this
  7.  
    }
  8.  
     
  9.  
    // additional parameters
  10.  
    var args = toArray(arguments, 1);
  11.  
    // 把 this(也就是Vue)作为数组的第一项
  12.  
    args.unshift(this);
  13.  
    // 如果插件的install属性是函数,调用它
  14.  
    if (typeof plugin.install === 'function') {
  15.  
    plugin.install.apply(plugin, args);
  16.  
    } else if (typeof plugin === 'function') {
  17.  
    // 如果插件是函数,则调用它
  18.  
    // apply(null) 严格模式下 plugin 插件函数的 this 就是 null
  19.  
    plugin.apply(null, args);
  20.  
    }
  21.  
    // 添加到已安装的插件
  22.  
    installedPlugins.push(plugin);
  23.  
    return this
  24.  
    };
  25.  
    }

install 函数

vuex/src/store.js

  1.  
    export function install (_Vue) {
  2.  
    // Vue 已经存在并且相等,说明已经Vuex.use过
  3.  
    if (Vue && _Vue === Vue) {
  4.  
    // 省略代码:非生产环境报错,vuex已经安装
  5.  
    return
  6.  
    }
  7.  
    Vue = _Vue
  8.  
    applyMixin(Vue)
  9.  
    }

接下来看 applyMixin 函数

applyMixin 函数

vuex/src/mixin.js

  1.  
    export default function (Vue) {
  2.  
    // Vue 版本号
  3.  
    const version = Number(Vue.version.split('.')[0])
  4.  
    if (version >= 2) {
  5.  
    // 合并选项后 beforeCreate 是数组里函数的形式 [ƒ, ƒ]
  6.  
    // 最后调用循环遍历这个数组,调用这些函数,这是一种函数与函数合并的解决方案。
  7.  
    // 假设是我们自己来设计,会是什么方案呢。
  8.  
    Vue.mixin({ beforeCreate: vuexInit })
  9.  
    } else {
  10.  
    // 省略1.x的版本代码 ...
  11.  
    }
  12.  
     
  13.  
    /**
  14.  
    * Vuex init hook, injected into each instances init hooks list.
  15.  
    */
  16.  
    function vuexInit () {
  17.  
    const options = this.$options
  18.  
    // store injection
  19.  
    // store 注入到每一个Vue的实例中
  20.  
    if (options.store) {
  21.  
    this.$store = typeof options.store === 'function'
  22.  
    ? options.store()
  23.  
    : options.store
  24.  
    } else if (options.parent && options.parent.$store) {
  25.  
    this.$store = options.parent.$store
  26.  
    }
  27.  
    }
  28.  
    }

最终每个Vue的实例对象,都有一个$store属性。且是同一个Store实例。
用购物车的例子来举例就是:

  1.  
    const vm = new Vue({
  2.  
    el: '#app',
  3.  
    store,
  4.  
    render: h => h(App)
  5.  
    })
  6.  
    console.log('vm.$store === vm.$children[0].$store', vm.$store === vm.$children[0].$store)
  7.  
    // true
  8.  
    console.log('vm.$store === vm.$children[0].$children[0].$store', vm.$store === vm.$children[0].$children[0].$store)
  9.  
    // true
  10.  
    console.log('vm.$store === vm.$children[0].$children[1].$store', vm.$store === vm.$children[0].$children[1].$store)
  11.  
    // true

Vuex.Store 构造函数

先看最终 new Vuex.Store 之后的 Store 实例对象关系图:先大致有个印象。

 

fc2b0824a97717221fce1ccecbfc6d13.png
  1.  
    export class Store {
  2.  
    constructor (options = {}) {
  3.  
    // 这个构造函数比较长,这里省略,后文分开细述
  4.  
    }
  5.  
    }
  6.  
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
  7.  
    install(window.Vue)
  8.  
    }

如果是 cdn script 方式引入vuex插件,则自动安装vuex插件,不需要用Vue.use(Vuex)来安装。

  1.  
    // asset 函数实现
  2.  
    export function assert (condition, msg) {
  3.  
    if (!condition) throw new Error(`[vuex] ${msg}`)
  4.  
    }
  5.  
    if (process.env.NODE_ENV !== 'production') {
  6.  
    // 可能有读者会问:为啥不用 console.assert,console.assert 函数报错不会阻止后续代码执行
  7.  
    assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
  8.  
    assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
  9.  
    assert(this instanceof Store, `store must be called with the new operator.`)
  10.  
    }

条件断言:不满足直接抛出错误

1.必须使用 Vue.use(Vuex) 创建 store 实例。 
2.当前环境不支持 Promise,报错: vuex 需要 Promise polyfill。 
3. Store 函数必须使用 new 操作符调用。
  1.  
    const {
  2.  
    // 插件默认是空数组
  3.  
    plugins = [],
  4.  
    // 严格模式默认是false
  5.  
    strict = false
  6.  
    } = options

从用户定义的new Vuex.Store(options) 取出pluginsstrict参数。

  1.  
    // store internal state
  2.  
    // store 实例对象 内部的 state
  3.  
    this._committing = false
  4.  
    // 用来存放处理后的用户自定义的actoins
  5.  
    this._actions = Object.create(null)
  6.  
    // 用来存放 actions 订阅
  7.  
    this._actionSubscribers = []
  8.  
    // 用来存放处理后的用户自定义的mutations
  9.  
    this._mutations = Object.create(null)
  10.  
    // 用来存放处理后的用户自定义的 getters
  11.  
    this._wrappedGetters = Object.create(null)
  12.  
    // 模块收集器,构造模块树形结构
  13.  
    this._modules = new ModuleCollection(options)
  14.  
    // 用于存储模块命名空间的关系
  15.  
    this._modulesNamespaceMap = Object.create(null)
  16.  
    // 订阅
  17.  
    this._subscribers = []
  18.  
    // 用于使用 $watch 观测 getters
  19.  
    this._watcherVM = new Vue()
  20.  
    // 用来存放生成的本地 getters 的缓存
  21.  
    this._makeLocalGettersCache = Object.create(null)

声明Store实例对象一些内部变量。用于存放处理后用户自定义的actionsmutationsgetters等变量。

提一下 Object.create(null) 和 {} 的区别。前者没有原型链,后者有。 即Object.create(null).__proto__是 undefined ({}).__proto__ 是 Object.prototype
  1.  
    // bind commit and dispatch to self
  2.  
    const store = this
  3.  
    const { dispatch, commit } = this
  4.  
    this.dispatch = function boundDispatch (type, payload) {
  5.  
    return dispatch.call(store, type, payload)
  6.  
    }
  7.  
    this.commit = function boundCommit (type, payload, options) {
  8.  
    return commit.call(store, type, payload, options)
  9.  
    }

给自己 绑定 commit 和 dispatch

为何要这样绑定 ? 
说明调用 commit 和 dispach 的 this 不一定是 store 实例 
这是确保这两个函数里的 this 是 store 实例
  1.  
    // 严格模式,默认是false
  2.  
    this.strict = strict
  3.  
    // 根模块的state
  4.  
    const state = this._modules.root.state
  5.  
    // init root module.
  6.  
    // this also recursively registers all sub-modules
  7.  
    // and collects all module getters inside this._wrappedGetters
  8.  
    installModule(this, state, [], this._modules.root)
  9.  
    // initialize the store vm, which is responsible for the reactivity
  10.  
    // (also registers _wrappedGetters as computed properties)
  11.  
    resetStoreVM(this, state)

上述这段代码 installModule(this, state, [], this._modules.root)

初始化 根模块。 
并且也递归的注册所有子模块。 
并且收集所有模块的 getters 放在 this._wrappedGetters 里面。

resetStoreVM(this, state)

初始化 store._vm 响应式的 
并且注册 _wrappedGetters 作为 computed 的属性
plugins.forEach(plugin => plugin(this))

插件:把实例对象 store 传给插件函数,执行所有插件。

  1.  
    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
  2.  
    if (useDevtools) {
  3.  
    devtoolPlugin(this)
  4.  
    }

初始化 vue-devtool 开发工具。
参数 devtools 传递了取 devtools 否则取Vue.config.devtools 配置。

初读这个构造函数的全部源代码。会发现有三个地方需要重点看。分别是:

  1.  
    this._modules = new ModuleCollection(options)
  2.  
    installModule(this, state, [], this._modules.root)
  3.  
    resetStoreVM(this, state)

阅读时可以断点调试,赋值语句this._modules = new ModuleCollection(options),如果暂时不想看,可以直接看返回结果。installModuleresetStoreVM函数则可以断点调试。

class ModuleCollection

收集模块,构造模块树结构。

注册根模块 参数 rawRootModule 也就是 Vuex.Store 的 options 参数 
未加工过的模块(用户自定义的),根模块
  1.  
    export default class ModuleCollection {
  2.  
    constructor (rawRootModule) {
  3.  
    // register root module (Vuex.Store options)
  4.  
    this.register([], rawRootModule, false)
  5.  
    }
  6.  
    }
  7.  
    /**
  8.  
    * 注册模块
  9.  
    * @param {Array} path 路径
  10.  
    * @param {Object} rawModule 原始未加工的模块
  11.  
    * @param {Boolean} runtime runtime 默认是 true
  12.  
    */
  13.  
    register (path, rawModule, runtime = true) {
  14.  
    // 非生产环境 断言判断用户自定义的模块是否符合要求
  15.  
    if (process.env.NODE_ENV !== 'production') {
  16.  
    assertRawModule(path, rawModule)
  17.  
    }
  18.  
     
  19.  
    const newModule = new Module(rawModule, runtime)
  20.  
    if (path.length === 0) {
  21.  
    this.root = newModule
  22.  
    } else {
  23.  
    const parent = this.get(path.slice(0, -1))
  24.  
    parent.addChild(path[path.length - 1], newModule)
  25.  
    }
  26.  
     
  27.  
    // register nested modules
  28.  
    // 递归注册子模块
  29.  
    if (rawModule.modules) {
  30.  
    forEachValue(rawModule.modules, (rawChildModule, key) => {
  31.  
    this.register(path.concat(key), rawChildModule, runtime)
  32.  
    })
  33.  
    }
  34.  
    }

class Module

  1.  
    // Base data struct for store's module, package with some attribute and method
  2.  
    // store 的模块 基础数据结构,包括一些属性和方法
  3.  
    export default class Module {
  4.  
    constructor (rawModule, runtime) {
  5.  
    // 接收参数 runtime
  6.  
    this.runtime = runtime
  7.  
    // Store some children item
  8.  
    // 存储子模块
  9.  
    this._children = Object.create(null)
  10.  
    // Store the origin module object which passed by programmer
  11.  
    // 存储原始未加工的模块
  12.  
    this._rawModule = rawModule
  13.  
    // 模块 state
  14.  
    const rawState = rawModule.state
  15.  
     
  16.  
    // Store the origin module's state
  17.  
    // 原始Store 可能是函数,也可能是是对象,是假值,则赋值空对象。
  18.  
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  19.  
    }
  20.  
    }

经过一系列的注册后,最后 this._modules = new ModuleCollection(options) this._modules 的值是这样的。 笔者画了一张图表示:

 

d48998747baea56d61d55f81f525db29.png

installModule 函数

  1.  
    function installModule (store, rootState, path, module, hot) {
  2.  
    // 是根模块
  3.  
    const isRoot = !path.length
  4.  
    // 命名空间 字符串
  5.  
    const namespace = store._modules.getNamespace(path)
  6.  
    if (module.namespaced) {
  7.  
    // 省略代码: 模块命名空间map对象中已经有了,开发环境报错提示重复
  8.  
    // module 赋值给 _modulesNamespaceMap[namespace]
  9.  
    store._modulesNamespaceMap[namespace] = module
  10.  
    }
  11.  
    // ... 后续代码 移出来 待读解释
  12.  
    }

注册 state

  1.  
    // set state
  2.  
    // 不是根模块且不是热重载
  3.  
    if (!isRoot && !hot) {
  4.  
    // 获取父级的state
  5.  
    const parentState = getNestedState(rootState, path.slice(0, -1))
  6.  
    // 模块名称
  7.  
    // 比如 cart
  8.  
    const moduleName = path[path.length - 1]
  9.  
    // state 注册
  10.  
    store._withCommit(() => {
  11.  
    // 省略代码:非生产环境 报错 模块 state 重复设置
  12.  
    Vue.set(parentState, moduleName, module.state)
  13.  
    })
  14.  
    }

最后得到的是类似这样的结构且是响应式的数据 实例 Store.state 比如:

  1.  
    {
  2.  
    // 省略若干属性和方法
  3.  
    // 这里的 state 是只读属性 可搜索 get state 查看,上文写过
  4.  
    state: {
  5.  
    cart: {
  6.  
    checkoutStatus: null,
  7.  
    items: []
  8.  
    }
  9.  
    }
  10.  
    }
  11.  
    const local = module.context = makeLocalContext(store, namespace, path)
module.context 这个赋值主要是给 helpers 中 mapState、 mapGetters、 mapMutationsmapActions四个辅助函数使用的。 
生成本地的dispatch、commit、getters和state。 
主要作用就是抹平差异化,不需要用户再传模块参数。

遍历注册 mutation

  1.  
    module.forEachMutation((mutation, key) => {
  2.  
    const namespacedType = namespace + key
  3.  
    registerMutation(store, namespacedType, mutation, local)
  4.  
    })
  5.  
    /**
  6.  
    * 注册 mutation
  7.  
    * @param {Object} store 对象
  8.  
    * @param {String} type 类型
  9.  
    * @param {Function} handler 用户自定义的函数
  10.  
    * @param {Object} local local 对象
  11.  
    */
  12.  
    function registerMutation (store, type, handler, local) {
  13.  
    // 收集的所有的mutations找对应的mutation函数,没有就赋值空数组
  14.  
    const entry = store._mutations[type] || (store._mutations[type] = [])
  15.  
    // 最后 mutation
  16.  
    entry.push(function wrappedMutationHandler (payload) {
  17.  
    /**
  18.  
    * mutations: {
  19.  
    * pushProductToCart (state, { id }) {
  20.  
    * console.log(state);
  21.  
    * }
  22.  
    * }
  23.  
    * 也就是为什么用户定义的 mutation 第一个参数是state的原因,第二个参数是payload参数
  24.  
    */
  25.  
    handler.call(store, local.state, payload)
  26.  
    })
  27.  
    }

遍历注册 action

  1.  
    module.forEachAction((action, key) => {
  2.  
    const type = action.root ? key : namespace + key
  3.  
    const handler = action.handler || action
  4.  
    registerAction(store, type, handler, local)
  5.  
    })
  6.  
    /**
  7.  
    * 注册 mutation
  8.  
    * @param {Object} store 对象
  9.  
    * @param {String} type 类型
  10.  
    * @param {Function} handler 用户自定义的函数
  11.  
    * @param {Object} local local 对象
  12.  
    */
  13.  
    function registerAction (store, type, handler, local) {
  14.  
    const entry = store._actions[type] || (store._actions[type] = [])
  15.  
    // payload 是actions函数的第二个参数
  16.  
    entry.push(function wrappedActionHandler (payload) {
  17.  
    /**
  18.  
    * 也就是为什么用户定义的actions中的函数第一个参数有
  19.  
    * { dispatch, commit, getters, state, rootGetters, rootState } 的原因
  20.  
    * actions: {
  21.  
    * checkout ({ commit, state }, products) {
  22.  
    * console.log(commit, state);
  23.  
    * }
  24.  
    * }
  25.  
    */
  26.  
    let res = handler.call(store, {
  27.  
    dispatch: local.dispatch,
  28.  
    commit: local.commit,
  29.  
    getters: local.getters,
  30.  
    state: local.state,
  31.  
    rootGetters: store.getters,
  32.  
    rootState: store.state
  33.  
    }, payload)
  34.  
    /**
  35.  
    * export function isPromise (val) {
  36.  
    return val && typeof val.then === 'function'
  37.  
    }
  38.  
    * 判断如果不是Promise Promise 化,也就是为啥 actions 中处理异步函数
  39.  
    也就是为什么构造函数中断言不支持promise报错的原因
  40.  
    vuex需要Promise polyfill
  41.  
    assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
  42.  
    */
  43.  
    if (!isPromise(res)) {
  44.  
    res = Promise.resolve(res)
  45.  
    }
  46.  
    // devtool 工具触发 vuex:error
  47.  
    if (store._devtoolHook) {
  48.  
    // catch 捕获错误
  49.  
    return res.catch(err => {
  50.  
    store._devtoolHook.emit('vuex:error', err)
  51.  
    // 抛出错误
  52.  
    throw err
  53.  
    })
  54.  
    } else {
  55.  
    // 然后函数执行结果
  56.  
    return res
  57.  
    }
  58.  
    })
  59.  
    }

遍历注册 getter

  1.  
    module.forEachGetter((getter, key) => {
  2.  
    const namespacedType = namespace + key
  3.  
    registerGetter(store, namespacedType, getter, local)
  4.  
    })
  5.  
    /**
  6.  
    * 注册 getter
  7.  
    * @param {Object} store Store实例
  8.  
    * @param {String} type 类型
  9.  
    * @param {Object} rawGetter 原始未加工的 getter 也就是用户定义的 getter 函数
  10.  
    * @examples 比如 cartProducts: (state, getters, rootState, rootGetters) => {}
  11.  
    * @param {Object} local 本地 local 对象
  12.  
    */
  13.  
    function registerGetter (store, type, rawGetter, local) {
  14.  
    // 类型如果已经存在,报错:已经存在
  15.  
    if (store._wrappedGetters[type]) {
  16.  
    if (process.env.NODE_ENV !== 'production') {
  17.  
    console.error(`[vuex] duplicate getter key: ${type}`)
  18.  
    }
  19.  
    return
  20.  
    }
  21.  
    // 否则:赋值
  22.  
    store._wrappedGetters[type] = function wrappedGetter (store) {
  23.  
    /**
  24.  
    * 这也就是为啥 getters 中能获取到 (state, getters, rootState, rootGetters) 这些值的原因
  25.  
    * getters = {
  26.  
    * cartProducts: (state, getters, rootState, rootGetters) => {
  27.  
    * console.log(state, getters, rootState, rootGetters);
  28.  
    * }
  29.  
    * }
  30.  
    */
  31.  
    return rawGetter(
  32.  
    local.state, // local state
  33.  
    local.getters, // local getters
  34.  
    store.state, // root state
  35.  
    store.getters // root getters
  36.  
    )
  37.  
    }
  38.  
    }

遍历注册 子模块

  1.  
    module.forEachChild((child, key) => {
  2.  
    installModule(store, rootState, path.concat(key), child, hot)
  3.  
    })

resetStoreVM 函数

resetStoreVM(this, state, hot)

初始化 store._vm 响应式的 
并且注册 _wrappedGetters 作为 computed 的属性
  1.  
    function resetStoreVM (store, state, hot) {
  2.  
     
  3.  
    // 存储一份老的Vue实例对象 _vm
  4.  
    const oldVm = store._vm
  5.  
     
  6.  
    // bind store public getters
  7.  
    // 绑定 store.getter
  8.  
    store.getters = {}
  9.  
    // reset local getters cache
  10.  
    // 重置 本地getters的缓存
  11.  
    store._makeLocalGettersCache = Object.create(null)
  12.  
    // 注册时收集的处理后的用户自定义的 wrappedGetters
  13.  
    const wrappedGetters = store._wrappedGetters
  14.  
    // 声明 计算属性 computed 对象
  15.  
    const computed = {}
  16.  
    // 遍历 wrappedGetters 赋值到 computed 上
  17.  
    forEachValue(wrappedGetters, (fn, key) => {
  18.  
    // use computed to leverage its lazy-caching mechanism
  19.  
    // direct inline function use will lead to closure preserving oldVm.
  20.  
    // using partial to return function with only arguments preserved in closure environment.
  21.  
    /**
  22.  
    * partial 函数
  23.  
    * 执行函数 返回一个新函数
  24.  
    export function partial (fn, arg) {
  25.  
    return function () {
  26.  
    return fn(arg)
  27.  
    }
  28.  
    }
  29.  
    */
  30.  
    computed[key] = partial(fn, store)
  31.  
    // getter 赋值 keys
  32.  
    Object.defineProperty(store.getters, key, {
  33.  
    get: () => store._vm[key],
  34.  
    // 可以枚举
  35.  
    enumerable: true // for local getters
  36.  
    })
  37.  
    })
  38.  
     
  39.  
    // use a Vue instance to store the state tree
  40.  
    // suppress warnings just in case the user has added
  41.  
    // some funky global mixins
  42.  
    // 使用一个 Vue 实例对象存储 state 树
  43.  
    // 阻止警告 用户添加的一些全局mixins
  44.  
     
  45.  
    // 声明变量 silent 存储用户设置的静默模式配置
  46.  
    const silent = Vue.config.silent
  47.  
    // 静默模式开启
  48.  
    Vue.config.silent = true
  49.  
    store._vm = new Vue({
  50.  
    data: {
  51.  
    $$state: state
  52.  
    },
  53.  
    computed
  54.  
    })
  55.  
    // 把存储的静默模式配置赋值回来
  56.  
    Vue.config.silent = silent
  57.  
     
  58.  
    // enable strict mode for new vm
  59.  
    // 开启严格模式 执行这句
  60.  
    // 用 $watch 观测 state,只能使用 mutation 修改 也就是 _withCommit 函数
  61.  
    if (store.strict) {
  62.  
    enableStrictMode(store)
  63.  
    }
  64.  
     
  65.  
    // 如果存在老的 _vm 实例
  66.  
    if (oldVm) {
  67.  
    // 热加载为 true
  68.  
    if (hot) {
  69.  
    // dispatch changes in all subscribed watchers
  70.  
    // to force getter re-evaluation for hot reloading.
  71.  
    // 设置 oldVm._data.$$state = null
  72.  
    store._withCommit(() => {
  73.  
    oldVm._data.$$state = null
  74.  
    })
  75.  
    }
  76.  
    // 实例销毁
  77.  
    Vue.nextTick(() => oldVm.$destroy())
  78.  
    }
  79.  
    }

到此,构造函数源代码看完了,接下来看 Vuex.Store 的 一些 API 实现。

Vuex.Store 实例方法

Vuex API 文档

commit

提交 mutation

  1.  
    commit (_type, _payload, _options) {
  2.  
    // check object-style commit
  3.  
    // 统一成对象风格
  4.  
    const {
  5.  
    type,
  6.  
    payload,
  7.  
    options
  8.  
    } = unifyObjectStyle(_type, _payload, _options)
  9.  
     
  10.  
    const mutation = { type, payload }
  11.  
    // 取出处理后的用户定义 mutation
  12.  
    const entry = this._mutations[type]
  13.  
    // 省略 非生产环境的警告代码 ...
  14.  
    this._withCommit(() => {
  15.  
    // 遍历执行
  16.  
    entry.forEach(function commitIterator (handler) {
  17.  
    handler(payload)
  18.  
    })
  19.  
    })
  20.  
    // 订阅 mutation 执行
  21.  
    this._subscribers.forEach(sub => sub(mutation, this.state))
  22.  
     
  23.  
    // 省略 非生产环境的警告代码 ...
  24.  
    }

commit 支持多种方式。比如:

  1.  
    store.commit('increment', {
  2.  
    count: 10
  3.  
    })
  4.  
    // 对象提交方式
  5.  
    store.commit({
  6.  
    type: 'increment',
  7.  
    count: 10
  8.  
    })

unifyObjectStyle函数将参数统一,返回 { type, payload, options }

dispatch

分发 action

  1.  
    dispatch (_type, _payload) {
  2.  
    // check object-style dispatch
  3.  
    // 获取到type和payload参数
  4.  
    const {
  5.  
    type,
  6.  
    payload
  7.  
    } = unifyObjectStyle(_type, _payload)
  8.  
     
  9.  
    // 声明 action 变量 等于 type和payload参数
  10.  
    const action = { type, payload }
  11.  
    // 入口,也就是 _actions 集合
  12.  
    const entry = this._actions[type]
  13.  
    // 省略 非生产环境的警告代码 ...
  14.  
    try {
  15.  
    this._actionSubscribers
  16.  
    .filter(sub => sub.before)
  17.  
    .forEach(sub => sub.before(action, this.state))
  18.  
    } catch (e) {
  19.  
    if (process.env.NODE_ENV !== 'production') {
  20.  
    console.warn(`[vuex] error in before action subscribers: `)
  21.  
    console.error(e)
  22.  
    }
  23.  
    }
  24.  
     
  25.  
    const result = entry.length > 1
  26.  
    ? Promise.all(entry.map(handler => handler(payload)))
  27.  
    : entry[0](payload)
  28.  
     
  29.  
    return result.then(res => {
  30.  
    try {
  31.  
    this._actionSubscribers
  32.  
    .filter(sub => sub.after)
  33.  
    .forEach(sub => sub.after(action, this.state))
  34.  
    } catch (e) {
  35.  
    if (process.env.NODE_ENV !== 'production') {
  36.  
    console.warn(`[vuex] error in after action subscribers: `)
  37.  
    console.error(e)
  38.  
    }
  39.  
    }
  40.  
    return res
  41.  
    })
  42.  
    }

replaceState

替换 store 的根状态,仅用状态合并或时光旅行调试。

  1.  
    replaceState (state) {
  2.  
    this._withCommit(() => {
  3.  
    this._vm._data.$$state = state
  4.  
    })
  5.  
    }

watch

响应式地侦听 fn 的返回值,当值改变时调用回调函数。

  1.  
    /**
  2.  
    * 观测某个值
  3.  
    * @param {Function} getter 函数
  4.  
    * @param {Function} cb 回调
  5.  
    * @param {Object} options 参数对象
  6.  
    */
  7.  
    watch (getter, cb, options) {
  8.  
    if (process.env.NODE_ENV !== 'production') {
  9.  
    assert(typeof getter === 'function', `store.watch only accepts a function.`)
  10.  
    }
  11.  
    return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  12.  
    }

subscribe

订阅 store 的 mutation

  1.  
    subscribe (fn) {
  2.  
    return genericSubscribe(fn, this._subscribers)
  3.  
    }
  4.  
    // 收集订阅者
  5.  
    function genericSubscribe (fn, subs) {
  6.  
    if (subs.indexOf(fn) < 0) {
  7.  
    subs.push(fn)
  8.  
    }
  9.  
    return () => {
  10.  
    const i = subs.indexOf(fn)
  11.  
    if (i > -1) {
  12.  
    subs.splice(i, 1)
  13.  
    }
  14.  
    }
  15.  
    }

subscribeAction

订阅 store 的 action

  1.  
    subscribeAction (fn) {
  2.  
    const subs = typeof fn === 'function' ? { before: fn } : fn
  3.  
    return genericSubscribe(subs, this._actionSubscribers)
  4.  
    }

registerModule

注册一个动态模块。

  1.  
    /**
  2.  
    * 动态注册模块
  3.  
    * @param {Array|String} path 路径
  4.  
    * @param {Object} rawModule 原始未加工的模块
  5.  
    * @param {Object} options 参数选项
  6.  
    */
  7.  
    registerModule (path, rawModule, options = {}) {
  8.  
    // 如果 path 是字符串,转成数组
  9.  
    if (typeof path === 'string') path = [path]
  10.  
     
  11.  
    // 省略 非生产环境 报错代码
  12.  
     
  13.  
    // 手动调用 模块注册的方法
  14.  
    this._modules.register(path, rawModule)
  15.  
    // 安装模块
  16.  
    installModule(this, this.state, path, this._modules.get(path), options.preserveState)
  17.  
    // reset store to update getters...
  18.  
    // 设置 resetStoreVM
  19.  
    resetStoreVM(this, this.state)
  20.  
    }

unregisterModule

卸载一个动态模块。

  1.  
    /**
  2.  
    * 注销模块
  3.  
    * @param {Array|String} path 路径
  4.  
    */
  5.  
    unregisterModule (path) {
  6.  
    // 如果 path 是字符串,转成数组
  7.  
    if (typeof path === 'string') path = [path]
  8.  
     
  9.  
    // 省略 非生产环境 报错代码 ...
  10.  
     
  11.  
    // 手动调用模块注销
  12.  
    this._modules.unregister(path)
  13.  
    this._withCommit(() => {
  14.  
    // 注销这个模块
  15.  
    const parentState = getNestedState(this.state, path.slice(0, -1))
  16.  
    Vue.delete(parentState, path[path.length - 1])
  17.  
    })
  18.  
    // 重置 Store
  19.  
    resetStore(this)
  20.  
    }

hotUpdate

热替换新的 action 和 mutation

  1.  
    // 热加载
  2.  
    hotUpdate (newOptions) {
  3.  
    // 调用的是 ModuleCollection 的 update 方法,最终调用对应的是每个 Module 的 update
  4.  
    this._modules.update(newOptions)
  5.  
    // 重置 Store
  6.  
    resetStore(this, true)
  7.  
    }

组件绑定的辅助函数

文件路径:vuex/src/helpers.js

mapState

为组件创建计算属性以返回 Vuex store 中的状态。

  1.  
    export const mapState = normalizeNamespace((namespace, states) => {
  2.  
    const res = {}
  3.  
    // 非生产环境 判断参数 states 必须是数组或者是对象
  4.  
    if (process.env.NODE_ENV !== 'production' && !isValidMap(states)) {
  5.  
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  6.  
    }
  7.  
    normalizeMap(states).forEach(({ key, val }) => {
  8.  
    res[key] = function mappedState () {
  9.  
    let state = this.$store.state
  10.  
    let getters = this.$store.getters
  11.  
    // 传了参数 namespace
  12.  
    if (namespace) {
  13.  
    // 用 namespace 从 store 中找一个模块。
  14.  
    const module = getModuleByNamespace(this.$store, 'mapState', namespace)
  15.  
    if (!module) {
  16.  
    return
  17.  
    }
  18.  
    state = module.context.state
  19.  
    getters = module.context.getters
  20.  
    }
  21.  
    return typeof val === 'function'
  22.  
    ? val.call(this, state, getters)
  23.  
    : state[val]
  24.  
    }
  25.  
    // 标记为 vuex 方便在 devtools 显示
  26.  
    // mark vuex getter for devtools
  27.  
    res[key].vuex = true
  28.  
    })
  29.  
    return res
  30.  
    })

normalizeNamespace 标准化统一命名空间

  1.  
    function normalizeNamespace (fn) {
  2.  
    return (namespace, map) => {
  3.  
    // 命名空间没传,交换参数,namespace 为空字符串
  4.  
    if (typeof namespace !== 'string') {
  5.  
    map = namespace
  6.  
    namespace = ''
  7.  
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
  8.  
    // 如果是字符串,最后一个字符不是 / 添加 /
  9.  
    // 因为 _modulesNamespaceMap 存储的是这样的结构。
  10.  
    /**
  11.  
    * _modulesNamespaceMap:
  12.  
    cart/: {}
  13.  
    products/: {}
  14.  
    }
  15.  
    * */
  16.  
    namespace += '/'
  17.  
    }
  18.  
    return fn(namespace, map)
  19.  
    }
  20.  
    }
  21.  
    // 校验是否是map 是数组或者是对象。
  22.  
    function isValidMap (map) {
  23.  
    return Array.isArray(map) || isObject(map)
  24.  
    }
  25.  
    /**
  26.  
    * Normalize the map
  27.  
    * 标准化统一 map,最终返回的是数组
  28.  
    * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
  29.  
    * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
  30.  
    * @param {Array|Object} map
  31.  
    * @return {Object}
  32.  
    */
  33.  
    function normalizeMap (map) {
  34.  
    if (!isValidMap(map)) {
  35.  
    return []
  36.  
    }
  37.  
    return Array.isArray(map)
  38.  
    ? map.map(key => ({ key, val: key }))
  39.  
    : Object.keys(map).map(key => ({ key, val: map[key] }))
  40.  
    }

module.context 这个赋值主要是给 helpers 中 mapStatemapGettersmapMutationsmapActions四个辅助函数使用的。

  1.  
    // 在构造函数中 installModule 中
  2.  
    const local = module.context = makeLocalContext(store, namespace, path)

这里就是抹平差异,不用用户传递命名空间,获取到对应的 commit、dispatch、state、和 getters

getModuleByNamespace

  1.  
    function getModuleByNamespace (store, helper, namespace) {
  2.  
    // _modulesNamespaceMap 这个变量在 class Store installModule 函数中赋值的
  3.  
    const module = store._modulesNamespaceMap[namespace]
  4.  
    if (process.env.NODE_ENV !== 'production' && !module) {
  5.  
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  6.  
    }
  7.  
    return module
  8.  
    }

看完这些,最后举个例子: vuex/examples/shopping-cart/components/ShoppingCart.vue

  1.  
    computed: {
  2.  
    ...mapState({
  3.  
    checkoutStatus: state => state.cart.checkoutStatus
  4.  
    }),
  5.  
    }

没有命名空间的情况下,最终会转换成这样

  1.  
    computed: {
  2.  
    checkoutStatus: this.$store.state.checkoutStatus
  3.  
    }

假设有命名空间'ruochuan',

  1.  
    computed: {
  2.  
    ...mapState('ruochuan', {
  3.  
    checkoutStatus: state => state.cart.checkoutStatus
  4.  
    }),
  5.  
    }

则会转换成:

  1.  
    computed: {
  2.  
    checkoutStatus: this.$store._modulesNamespaceMap.['ruochuan/'].context.checkoutStatus
  3.  
    }

mapGetters

为组件创建计算属性以返回 getter 的返回值。

  1.  
    export const mapGetters = normalizeNamespace((namespace, getters) => {
  2.  
    const res = {}
  3.  
    // 省略代码:非生产环境 判断参数 getters 必须是数组或者是对象
  4.  
    normalizeMap(getters).forEach(({ key, val }) => {
  5.  
    // The namespace has been mutated by normalizeNamespace
  6.  
    val = namespace + val
  7.  
    res[key] = function mappedGetter () {
  8.  
    if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
  9.  
    return
  10.  
    }
  11.  
    // 省略代码:匹配不到 getter
  12.  
    return this.$store.getters[val]
  13.  
    }
  14.  
    // mark vuex getter for devtools
  15.  
    res[key].vuex = true
  16.  
    })
  17.  
    return res
  18.  
    })

举例:

  1.  
    computed: {
  2.  
    ...mapGetters('cart', {
  3.  
    products: 'cartProducts',
  4.  
    total: 'cartTotalPrice'
  5.  
    })
  6.  
    },

最终转换成:

  1.  
    computed: {
  2.  
    products: this.$store.getters['cart/cartProducts'],
  3.  
    total: this.$store.getters['cart/cartTotalPrice'],
  4.  
    }

mapActions

创建组件方法分发 action

  1.  
    export const mapActions = normalizeNamespace((namespace, actions) => {
  2.  
    const res = {}
  3.  
    // 省略代码: 非生产环境 判断参数 actions 必须是数组或者是对象
  4.  
    normalizeMap(actions).forEach(({ key, val }) => {
  5.  
    res[key] = function mappedAction (...args) {
  6.  
    // get dispatch function from store
  7.  
    let dispatch = this.$store.dispatch
  8.  
    if (namespace) {
  9.  
    const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
  10.  
    if (!module) {
  11.  
    return
  12.  
    }
  13.  
    dispatch = module.context.dispatch
  14.  
    }
  15.  
    return typeof val === 'function'
  16.  
    ? val.apply(this, [dispatch].concat(args))
  17.  
    : dispatch.apply(this.$store, [val].concat(args))
  18.  
    }
  19.  
    })
  20.  
    return res
  21.  
    })

mapMutations

创建组件方法提交 mutation。 mapMutations 和 mapActions 类似,只是 dispatch 换成了 commit。

  1.  
    let commit = this.$store.commit
  2.  
    commit = module.context.commit
  3.  
    return typeof val === 'function'
  4.  
    ? val.apply(this, [commit].concat(args))
  5.  
    : commit.apply(this.$store, [val].concat(args))

vuex/src/helpers

mapMutationsmapActions 举例:

  1.  
    {
  2.  
    methods: {
  3.  
    ...mapMutations(['inc']),
  4.  
    ...mapMutations('ruochuan', ['dec']),
  5.  
    ...mapActions(['actionA'])
  6.  
    ...mapActions('ruochuan', ['actionB'])
  7.  
    }
  8.  
    }

最终转换成

  1.  
    {
  2.  
    methods: {
  3.  
    inc(...args){
  4.  
    return this.$store.dispatch.apply(this.$store, ['inc'].concat(args))
  5.  
    },
  6.  
    dec(...args){
  7.  
    return this.$store._modulesNamespaceMap.['ruochuan/'].context.dispatch.apply(this.$store, ['dec'].concat(args))
  8.  
    },
  9.  
    actionA(...args){
  10.  
    return this.$store.commit.apply(this.$store, ['actionA'].concat(args))
  11.  
    }
  12.  
    actionB(...args){
  13.  
    return this.$store._modulesNamespaceMap.['ruochuan/'].context.commit.apply(this.$store, ['actionB'].concat(args))
  14.  
    }
  15.  
    }
  16.  
    }

由此可见:这些辅助函数极大地方便了开发者。

createNamespacedHelpers

创建基于命名空间的组件绑定辅助函数。

  1.  
    export const createNamespacedHelpers = (namespace) => ({
  2.  
    // bind(null) 严格模式下,napState等的函数 this 指向就是 null
  3.  
    mapState: mapState.bind(null, namespace),
  4.  
    mapGetters: mapGetters.bind(null, namespace),
  5.  
    mapMutations: mapMutations.bind(null, namespace),
  6.  
    mapActions: mapActions.bind(null, namespace)
  7.  
    })

就是把这些辅助函数放在一个对象中。

插件

插件部分文件路径是:vuex/src/plugins/devtoolvuex/src/plugins/logger

文章比较长了,这部分就不再叙述。具体可以看笔者的仓库 vuex-analysis vuex/src/plugins/ 的源码注释。

总结

文章比较详细的介绍了vuexvue源码调试方法和 Vuex 原理。并且详细介绍了 Vuex.use 安装和 new Vuex.Store 初始化、Vuex.Store 的全部API(如dispatchcommit等)的实现和辅助函数 mapStatemapGetters、 mapActionsmapMutations createNamespacedHelpers

文章注释,在vuex-analysis源码仓库里基本都有注释分析,求个star。再次强烈建议要克隆代码下来。

git clone https://github.com/lxchuan12/vuex-analysis.git

先把 Store 实例打印出来,看具体结构,再结合实例断点调试,事半功倍。

Vuex 源码相对不多,打包后一千多行,非常值得学习,也比较容易看完。

如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持,万分感谢。

推荐阅读

vuex 官方文档
vuex github 仓库
美团明裔:Vuex框架原理与源码分析这篇文章强烈推荐,流程图画的很好
知乎黄轶:Vuex 2.0 源码分析这篇文章也强烈推荐,讲述的比较全面
小虫巨蟹:Vuex 源码解析(如何阅读源代码实践篇)这篇文章也强烈推荐,主要讲如何阅读源代码
染陌:Vuex 源码解析
网易考拉前端团队:Vuex 源码分析
yck:Vuex 源码深度解析
小生方勤:【前端词典】从源码解读 Vuex 注入 Vue 生命周期的过程

笔者精选文章

学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
学习 lodash 源码整体架构,打造属于自己的函数式编程类库
学习 underscore 源码整体架构,打造属于自己的函数式编程类库
学习 jQuery 源码整体架构,打造属于自己的 js 类库
面试官问:JS的继承
面试官问:JS的this指向
面试官问:能否模拟实现JS的call和apply方法
面试官问:能否模拟实现JS的bind方法
面试官问:能否模拟实现JS的new操作符

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客-若川,使用vuepress重构了,阅读体验可能更好些
掘金专栏,欢迎关注~segmentfault前端视野专栏,欢迎关注~知乎前端视野专栏,欢迎关注~github blog,相关源码和资源都放在这里,求个star^_^~

欢迎加微信交流 微信公众号

可能比较有趣的微信公众号,长按扫码关注。也可以加微信 lxchuan12,注明来源,拉您进【前端视野交流群】。

posted on 2022-03-04 20:22  byd张小伟  阅读(150)  评论(0)    收藏  举报