Skip to content

学习源码整体架构系列8篇之vuex源码,前端面试高频源码,微信搜索「若川视野」关注我,长期交流学习~

Notifications You must be signed in to change notification settings

ruochuan12/vuex-analysis

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

1. 前言

你好,我是若川,微信搜索「若川视野」关注我,专注前端技术分享。欢迎加我微信ruochuan12,加群交流学习。

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

本文仓库地址git clone https://github.com/lxchuan12/vuex-analysis.git

要是有人说到怎么读源码,正在读文章的你能推荐我的源码系列文章,那真是太好了

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

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

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

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

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

2. 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,最后一次commitba2ff3a32019-11-11 11:51 Ben Hutton
包含笔者的注释,便于理解。

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

// 新增devtool配置,便于调试
devtool: 'source-map',
output: {}
git clone https://github.com/lxchuan12/vuex-analysis.git
cd vuex
npm i
npm run dev

打开 http://localhost:8080/
点击你想打开的例子,例如:Shopping Cart => http://localhost:8080/shopping-cart/
打开控制面板 source 在左侧找到 webapck// . src 目录 store 文件 根据自己需求断点调试即可。

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

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

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

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

{
  "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap"
}
git clone https://github.com/vuejs/vue.git
cd vue
npm i
# 在 dist/vue.js 最后一行追加一行 //# sourceMappingURL=vue.js.map
npm run dev
# 新终端窗口
# 根目录下 全局安装http-server(一行命令启动服务的工具)
npm i -g http-server
hs -p 8100

# 在examples 文件夹中把引用的vuejs的index.html 文件 vue.min.js 改为 vue.js
# 或者把dist文件夹的 vue.min.js ,替换成npm run dev编译后的dist/vue.js 

# 浏览器打开 open http://localhost:8100/examples/

# 打开控制面板 source 在左侧找到  src 目录 即vue.js源码文件 根据自己需求断点调试即可。

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

正文开始~

3. vuex 原理

简单说明下 vuex 原理

<template>
<div>
  count {{$store.state.count}}
</div>
</template>

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

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

class Store{
  get state () {
    return this._vm._data.$$state
  }
}

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

function resetStoreVM (store, state, hot) {
  //  省略若干代码
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  //  省略若干代码
}

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

3. Vue.use 安装

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

Vuex 对象关系图

看到这里,恭喜你已经了解了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的源码。

function initUse (Vue) {
  Vue.use = function (plugin) {
    var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
    // 如果已经存在,则直接返回this也就是Vue
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    var args = toArray(arguments, 1);
    // 把 this(也就是Vue)作为数组的第一项
    args.unshift(this);
    // 如果插件的install属性是函数,调用它
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args);
    } else if (typeof plugin === 'function') {
      // 如果插件是函数,则调用它
      // apply(null) 严格模式下 plugin 插件函数的 this 就是 null
      plugin.apply(null, args);
    }
    // 添加到已安装的插件
    installedPlugins.push(plugin);
    return this
  };
}

3.1 install 函数

vuex/src/store.js

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

接下来看 applyMixin 函数

3.2 applyMixin 函数

vuex/src/mixin.js

export default function (Vue) {
  // Vue 版本号
  const version = Number(Vue.version.split('.')[0])
  if (version >= 2) {
    // 合并选项后 beforeCreate 是数组里函数的形式  [ƒ,  ƒ]
    // 最后调用循环遍历这个数组,调用这些函数,这是一种函数与函数合并的解决方案。
    // 假设是我们自己来设计,会是什么方案呢。
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // 省略1.x的版本代码 ...
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */
  function vuexInit () {
    const options = this.$options
    // store injection
    // store 注入到每一个Vue的实例中
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

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

const vm = new Vue({
  el: '#app',
  store,
  render: h => h(App)
})
console.log('vm.$store === vm.$children[0].$store', vm.$store === vm.$children[0].$store)
// true
console.log('vm.$store === vm.$children[0].$children[0].$store', vm.$store === vm.$children[0].$children[0].$store)
// true
console.log('vm.$store === vm.$children[0].$children[1].$store', vm.$store === vm.$children[0].$children[1].$store)
// true

4. Vuex.Store 构造函数

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

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

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

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

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

1.必须使用 Vue.use(Vuex) 创建 store 实例。
2.当前环境不支持Promise,报错:vuex 需要 Promise polyfill
3.Store 函数必须使用 new 操作符调用。

const {
  // 插件默认是空数组
  plugins = [],
  // 严格模式默认是false
  strict = false
} = options

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

// store internal state
// store 实例对象 内部的 state
this._committing = false
// 用来存放处理后的用户自定义的actoins
this._actions = Object.create(null)
// 用来存放 actions 订阅
this._actionSubscribers = []
// 用来存放处理后的用户自定义的mutations
this._mutations = Object.create(null)
// 用来存放处理后的用户自定义的 getters
this._wrappedGetters = Object.create(null)
// 模块收集器,构造模块树形结构
this._modules = new ModuleCollection(options)
// 用于存储模块命名空间的关系
this._modulesNamespaceMap = Object.create(null)
// 订阅
this._subscribers = []
// 用于使用 $watch 观测 getters
this._watcherVM = new Vue()
// 用来存放生成的本地 getters 的缓存
this._makeLocalGettersCache = Object.create(null)

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

提一下 Object.create(null){} 的区别。前者没有原型链,后者有。 即 Object.create(null).__proto__undefined ({}).__proto__Object.prototype

// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
  return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
  return commit.call(store, type, payload, options)
}

给自己 绑定 commitdispatch

为何要这样绑定 ?
说明调用 commitdispachthis 不一定是 store 实例
这是确保这两个函数里的 thisstore 实例

// 严格模式,默认是false
this.strict = strict
// 根模块的state
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
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 传给插件函数,执行所有插件。

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

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

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

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

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

4.1 class ModuleCollection

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

注册根模块 参数 rawRootModule 也就是 Vuex.Storeoptions 参数
未加工过的模块(用户自定义的),根模块

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }
}
/**
  * 注册模块
  * @param {Array} path 路径
  * @param {Object} rawModule 原始未加工的模块
  * @param {Boolean} runtime runtime 默认是 true
  */
register (path, rawModule, runtime = true) {
  // 非生产环境 断言判断用户自定义的模块是否符合要求
  if (process.env.NODE_ENV !== 'production') {
    assertRawModule(path, rawModule)
  }

  const newModule = new Module(rawModule, runtime)
  if (path.length === 0) {
    this.root = newModule
  } else {
    const parent = this.get(path.slice(0, -1))
    parent.addChild(path[path.length - 1], newModule)
  }

  // register nested modules
  // 递归注册子模块
  if (rawModule.modules) {
    forEachValue(rawModule.modules, (rawChildModule, key) => {
      this.register(path.concat(key), rawChildModule, runtime)
    })
  }
}

4.1.1 class Module

// Base data struct for store's module, package with some attribute and method
// store 的模块 基础数据结构,包括一些属性和方法
export default class Module {
  constructor (rawModule, runtime) {
    // 接收参数 runtime
    this.runtime = runtime
    // Store some children item
    // 存储子模块
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    // 存储原始未加工的模块
    this._rawModule = rawModule
    // 模块 state
    const rawState = rawModule.state

    // Store the origin module's state
    // 原始Store 可能是函数,也可能是是对象,是假值,则赋值空对象。
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
}

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

ModuleCollection

4.2 installModule 函数

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

4.2.1 注册 state

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

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

{
  // 省略若干属性和方法
  // 这里的 state 是只读属性 可搜索 get state 查看,上文写过
  state: {
    cart: {
      checkoutStatus: null,
      items: []
    }
 }
}
const local = module.context = makeLocalContext(store, namespace, path)

module.context 这个赋值主要是给 helpersmapStatemapGettersmapMutationsmapActions四个辅助函数使用的。
生成本地的dispatch、commit、getters和state。
主要作用就是抹平差异化,不需要用户再传模块参数。

4.2.2 遍历注册 mutation

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})
/**
 * 注册 mutation
 * @param {Object} store 对象
 * @param {String} type 类型
 * @param {Function} handler 用户自定义的函数
 * @param {Object} local local 对象
 */
function registerMutation (store, type, handler, local) {
  // 收集的所有的mutations找对应的mutation函数,没有就赋值空数组
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // 最后 mutation
  entry.push(function wrappedMutationHandler (payload) {
    /**
     * mutations: {
     *    pushProductToCart (state, { id }) {
     *        console.log(state);
     *    }
     * }
     * 也就是为什么用户定义的 mutation 第一个参数是state的原因,第二个参数是payload参数
     */
    handler.call(store, local.state, payload)
  })
}

4.2.3 遍历注册 action

module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key
  const handler = action.handler || action
  registerAction(store, type, handler, local)
})
/**
* 注册 mutation
* @param {Object} store 对象
* @param {String} type 类型
* @param {Function} handler 用户自定义的函数
* @param {Object} local local 对象
*/
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  // payload 是actions函数的第二个参数
  entry.push(function wrappedActionHandler (payload) {
    /**
     * 也就是为什么用户定义的actions中的函数第一个参数有
     *  { dispatch, commit, getters, state, rootGetters, rootState } 的原因
     * actions: {
     *    checkout ({ commit, state }, products) {
     *        console.log(commit, state);
     *    }
     * }
     */
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    /**
     * export function isPromise (val) {
        return val && typeof val.then === 'function'
      }
     * 判断如果不是Promise Promise 化,也就是为啥 actions 中处理异步函数
        也就是为什么构造函数中断言不支持promise报错的原因
        vuex需要Promise polyfill
        assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
     */
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    // devtool 工具触发 vuex:error
    if (store._devtoolHook) {
      // catch 捕获错误
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        // 抛出错误
        throw err
      })
    } else {
      // 然后函数执行结果
      return res
    }
  })
}

4.2.4 遍历注册 getter

module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})
/**
 * 注册 getter
 * @param {Object} store  Store实例
 * @param {String} type 类型
 * @param {Object} rawGetter  原始未加工的 getter 也就是用户定义的 getter 函数
 * @examples  比如 cartProducts: (state, getters, rootState, rootGetters) => {}
 * @param {Object} local 本地 local 对象
 */
function registerGetter (store, type, rawGetter, local) {
  // 类型如果已经存在,报错:已经存在
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  // 否则:赋值
  store._wrappedGetters[type] = function wrappedGetter (store) {
    /**
     * 这也就是为啥 getters 中能获取到  (state, getters, rootState, rootGetters)  这些值的原因
     * getters = {
     *      cartProducts: (state, getters, rootState, rootGetters) => {
     *        console.log(state, getters, rootState, rootGetters);
     *      }
     * }
     */
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

4.2.5 遍历注册 子模块

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

4.3 resetStoreVM 函数

resetStoreVM(this, state, hot)

初始化 store._vm 响应式的
并且注册 _wrappedGetters 作为 computed 的属性

function resetStoreVM (store, state, hot) {

  // 存储一份老的Vue实例对象 _vm
  const oldVm = store._vm

  // bind store public getters
  // 绑定 store.getter
  store.getters = {}
  // reset local getters cache
  // 重置 本地getters的缓存
  store._makeLocalGettersCache = Object.create(null)
  // 注册时收集的处理后的用户自定义的 wrappedGetters
  const wrappedGetters = store._wrappedGetters
  // 声明 计算属性 computed 对象
  const computed = {}
  // 遍历 wrappedGetters 赋值到 computed 上
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure environment.
    /**
     * partial 函数
     * 执行函数 返回一个新函数
        export function partial (fn, arg) {
          return function () {
            return fn(arg)
          }
        }
     */
    computed[key] = partial(fn, store)
    // getter 赋值 keys
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      // 可以枚举
      enumerable: true // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  // 使用一个 Vue 实例对象存储 state 树
  // 阻止警告 用户添加的一些全局mixins

  // 声明变量 silent 存储用户设置的静默模式配置
  const silent = Vue.config.silent
  // 静默模式开启
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  // 把存储的静默模式配置赋值回来
  Vue.config.silent = silent

  // enable strict mode for new vm
  // 开启严格模式 执行这句
  // 用 $watch 观测 state,只能使用 mutation 修改 也就是 _withCommit 函数
  if (store.strict) {
    enableStrictMode(store)
  }

  // 如果存在老的 _vm 实例
  if (oldVm) {
    // 热加载为 true
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      // 设置  oldVm._data.$$state = null
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    // 实例销毁
    Vue.nextTick(() => oldVm.$destroy())
  }
}

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

5. Vuex.Store 实例方法

Vuex API 文档

5.1 commit

提交 mutation

commit (_type, _payload, _options) {
  // check object-style commit
  // 统一成对象风格
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  // 取出处理后的用户定义 mutation
  const entry = this._mutations[type]
  // 省略 非生产环境的警告代码 ...
  this._withCommit(() => {
    // 遍历执行
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })
  // 订阅 mutation 执行
  this._subscribers.forEach(sub => sub(mutation, this.state))

  // 省略 非生产环境的警告代码 ...
}

commit 支持多种方式。比如:

store.commit('increment', {
  count: 10
})
// 对象提交方式
store.commit({
  type: 'increment',
  count: 10
})

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

5.2 dispatch

分发 action

dispatch (_type, _payload) {
  // check object-style dispatch
  // 获取到type和payload参数
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)

  // 声明 action 变量 等于 type和payload参数
  const action = { type, payload }
  // 入口,也就是 _actions 集合
  const entry = this._actions[type]
  // 省略 非生产环境的警告代码 ...
  try {
    this._actionSubscribers
      .filter(sub => sub.before)
      .forEach(sub => sub.before(action, this.state))
  } catch (e) {
    if (process.env.NODE_ENV !== 'production') {
      console.warn(`[vuex] error in before action subscribers: `)
      console.error(e)
    }
  }

  const result = entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)

  return result.then(res => {
    try {
      this._actionSubscribers
        .filter(sub => sub.after)
        .forEach(sub => sub.after(action, this.state))
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in after action subscribers: `)
        console.error(e)
      }
    }
    return res
  })
}

5.3 replaceState

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

replaceState (state) {
  this._withCommit(() => {
    this._vm._data.$$state = state
  })
}

5.4 watch

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

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

5.5 subscribe

订阅 storemutation

subscribe (fn) {
  return genericSubscribe(fn, this._subscribers)
}
// 收集订阅者
function genericSubscribe (fn, subs) {
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

5.6 subscribeAction

订阅 storeaction

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

5.7 registerModule

注册一个动态模块。

/**
 * 动态注册模块
 * @param {Array|String} path 路径
 * @param {Object} rawModule 原始未加工的模块
 * @param {Object} options 参数选项
 */
registerModule (path, rawModule, options = {}) {
  // 如果 path 是字符串,转成数组
  if (typeof path === 'string') path = [path]

  // 省略 非生产环境 报错代码

  // 手动调用 模块注册的方法
  this._modules.register(path, rawModule)
  // 安装模块
  installModule(this, this.state, path, this._modules.get(path), options.preserveState)
  // reset store to update getters...
  // 设置 resetStoreVM
  resetStoreVM(this, this.state)
}

5.8 unregisterModule

卸载一个动态模块。

/**
 * 注销模块
 * @param {Array|String} path 路径
 */
unregisterModule (path) {
  // 如果 path 是字符串,转成数组
  if (typeof path === 'string') path = [path]

  // 省略 非生产环境 报错代码 ...

  // 手动调用模块注销
  this._modules.unregister(path)
  this._withCommit(() => {
    // 注销这个模块
    const parentState = getNestedState(this.state, path.slice(0, -1))
    Vue.delete(parentState, path[path.length - 1])
  })
  // 重置 Store
  resetStore(this)
}

5.9 hotUpdate

热替换新的 actionmutation

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

6. 组件绑定的辅助函数

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

6.1 mapState

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

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  // 非生产环境 判断参数 states  必须是数组或者是对象
  if (process.env.NODE_ENV !== 'production' && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      let state = this.$store.state
      let getters = this.$store.getters
      // 传了参数 namespace
      if (namespace) {
        // 用 namespace 从 store 中找一个模块。
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        state = module.context.state
        getters = module.context.getters
      }
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // 标记为 vuex 方便在 devtools 显示
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

normalizeNamespace 标准化统一命名空间

function normalizeNamespace (fn) {
  return (namespace, map) => {
    // 命名空间没传,交换参数,namespace 为空字符串
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      // 如果是字符串,最后一个字符不是 / 添加 /
      // 因为 _modulesNamespaceMap 存储的是这样的结构。
      /**
       * _modulesNamespaceMap:
          cart/: {}
          products/: {}
        }
       * */
      namespace += '/'
    }
    return fn(namespace, map)
  }
}
// 校验是否是map 是数组或者是对象。
function isValidMap (map) {
  return Array.isArray(map) || isObject(map)
}
/**
 * Normalize the map
 * 标准化统一 map,最终返回的是数组
 * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
 * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
 * @param {Array|Object} map
 * @return {Object}
 */
function normalizeMap (map) {
  if (!isValidMap(map)) {
    return []
  }
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

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

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

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

getModuleByNamespace

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

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

computed: {
    ...mapState({
      checkoutStatus: state => state.cart.checkoutStatus
    }),
}

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

computed: {
    checkoutStatus: this.$store.state.checkoutStatus
}

假设有命名空间'ruochuan',

computed: {
    ...mapState('ruochuan', {
      checkoutStatus: state => state.cart.checkoutStatus
    }),
}

则会转换成:

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

6.2 mapGetters

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

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  // 省略代码:非生产环境 判断参数 getters 必须是数组或者是对象
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      // 省略代码:匹配不到 getter
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

举例:

computed: {
  ...mapGetters('cart', {
    products: 'cartProducts',
    total: 'cartTotalPrice'
  })
},

最终转换成:

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

6.3 mapActions

创建组件方法分发 action

export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  // 省略代码: 非生产环境 判断参数 actions  必须是数组或者是对象
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      // get dispatch function from store
      let dispatch = this.$store.dispatch
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
        if (!module) {
          return
        }
        dispatch = module.context.dispatch
      }
      return typeof val === 'function'
        ? val.apply(this, [dispatch].concat(args))
        : dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

6.4 mapMutations

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

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

vuex/src/helpers

mapMutationsmapActions 举例:

{
  methods: {
    ...mapMutations(['inc']),
    ...mapMutations('ruochuan', ['dec']),
    ...mapActions(['actionA'])
    ...mapActions('ruochuan', ['actionB'])
  }
}

最终转换成

{
  methods: {
    inc(...args){
      return this.$store.dispatch.apply(this.$store, ['inc'].concat(args))
    },
    dec(...args){
      return this.$store._modulesNamespaceMap.['ruochuan/'].context.dispatch.apply(this.$store, ['dec'].concat(args))
    },
    actionA(...args){
      return this.$store.commit.apply(this.$store, ['actionA'].concat(args))
    }
    actionB(...args){
      return this.$store._modulesNamespaceMap.['ruochuan/'].context.commit.apply(this.$store, ['actionB'].concat(args))
    }
  }
}

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

6.5 createNamespacedHelpers

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

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

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

7. 插件

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

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

8. 总结

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

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

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

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

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

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

9. 推荐阅读

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

笔者另一个系列

面试官问:JS的继承
面试官问:JS的this指向
面试官问:能否模拟实现JS的call和apply方法
面试官问:能否模拟实现JS的bind方法
面试官问:能否模拟实现JS的new操作符

关于

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

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

可能比较有趣的微信公众号,长按扫码关注(回复pdf获取前端优质书籍pdf)。也可以加微信 ruochuan12,注明来源,拉您进【前端视野交流群】。

若川视野

About

学习源码整体架构系列8篇之vuex源码,前端面试高频源码,微信搜索「若川视野」关注我,长期交流学习~

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published