Vuex原来这么简单!!源码保姆级解读

1 Vuex介绍

Vuex是尤雨溪为vue编写的状态管理器。

在我们项目开发过程中,总是会有一部分信息是所有组件都需要访问到的,比如登录用户信息,如果仅通过父子组件之间通信来传递这些信息,那么当组件的数量到达一定规模之后,将会给你的代码带来噩梦级别的灾难。

由于现代框架都是以数据驱动视图,对于数据的管理就非常重要。Vuex就是为了解决这件事的,统一管理公用的状态,不仅使代码简洁易于维护,开发人员使用起来也更便捷。

阅读源码建议分以下步骤进行:

  1. 从全局思考:在对项目的的功能和使用有一定了解的情况下,站在全局的角度,明白该项目的目标是要解决什么问题,思考或者猜想作者是如何设计以实现该目标的。
  2. 从局部出发,以点破面:从最基础的配置使用出发,怎样的配置产生了怎样结果?配置和产生的结果之间有什么关系,从源码里面找到是如何实现这些关系的。
  3. 边实践边阅读:在阅读过程中,可能会遇到晦涩难懂的代码,比如看不懂函数的作用,那么可以尝试自己设计参数传入,看看返回什么结果,也可以在代码中打断点,查看该代码发挥了什么作用。
  4. 书读百遍,其意自现:刚开始看代码的时候,往往一头雾水,静下心来,多读几遍,肯定会慢慢地加深理解的。

下面就从一个简单的例子出发解读Vuex的源码。

2 从例子出发

js 复制代码
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    // 定义一个一级模块:user
    user: {
      namespaced: true,
      state: {
        name: 'lsm',
        age: 18,
      },
      getters: {},
      mutations: {
        setName: (state, name) => {
          state.name = name;
        },
        setAge: (state, age) => {
          state.age = age;
        },
      },
      actions: {},
      modules: {
        // 定义二级模块 privateUser
        privateUser: {
          namespaced: true,
          state: {
            type: 'private',
          },
          getters: {},
          mutations: {
            setType: (state, type) => {
              state.type = type;
            },
          },
          actions: {},
        },
        // 定义二级模块 publicUser
        publicUser: {
          namespaced: true,
          state: {
            type: 'public',
          },
          getters: {},
          mutations: {
            setType: (state, type) => {
              state.type = type;
            },
          },
          actions: {},
        },
      },
    }
  },
});

上述的代码非常简单,首先定义了一个一级模块user,又在user下面定义了两个二级模块privateUserpublicUser,并且namespace都填写了true

让我们将this.$store打印出来查看一下:

首先注意到我们代码里常用的commit,dispatch方法,以及gettersstate对象。 另外还有许多以下划线开头的内部变量,比如_modules_modulesNamespaceMap等等。

接下来让我们点开state_modules查看

state中,模块名称转为了对象的键名,并且拼接成嵌套对象。

_modules中,可以看到rootuserprivateUserpublicUser,并且他们也是嵌套结构,另外这四个都是Module类,可以猜想到两者之间一定有某些联系,并且代码中会有Module类。

最后再打开_modulesNamespaceMap查看一下内容,这不就是我们调用模块mutations或者actions时写的前缀吗?

this.$store.commit('user/setName','lsm'),想起来了吗?

3 带着这些疑问开始源码分析

Vuex源码地址

3.1 目录文件简介

arduino 复制代码
├─src
│  │  helpers.js    //语法糖函数,mapState,mapGetters,mapMutations,mapActions
│  │  index.cjs.js  
│  │  index.js      //三个index都是最终导出的文件
│  │  index.mjs
│  │  mixin.js      //安装到vue
│  │  store.js      //Store类
│  │  util.js       //项目中的通用方法
│  │
│  ├─module
│  │      module-collection.js  //模块收集类
│  │      module.js             //单个模块类
│  │
│  └─plugins
│          devtool.js           //devtool插件
│          logger.js            //日志插件

3.2 index.js

js 复制代码
export default {
  Store,        //Store类
  install,      //install方法
  version: '__VERSION__',
  mapState,     //四个语法糖函数
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}

从这里就可以看出为什么我们创建使用Vuex是这么写的了:

const store=new Vuex.Store({....})

另外Vue.use(Vuex),执行的install方法也找到了。

3.3 inatall

跳转代码,可以知道install实际执行的是mixin.js里的方法,作用是将store挂载到所有的vue组件上

js 复制代码
export default function (Vue) {
  // 获取vue主版本号
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    // 如果版本号为2及以上,在vue组件的beforeCreate钩子中混入vuexInit方法,
    // 也就是所有组件创建之前都会执行一次这个方法
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // 如果版本为1,则挂载到vue原型上
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */
  // 组件创建时,从$options或者父组件中获取store,设置给当前组件的this.$store
  // 这就是为什么所有组件都能够直接通过this.$store访问到vuex
  function vuexInit () {
    const options = this.$options
    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
    }
  }
}

3.4 Store类

js 复制代码
export class Store {
  constructor (options = {}) {
    // 初始化类实例的内部变量
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    // 此处需要特别注意,我们传入的options实际上是用来创建一个ModuleCollection实例,也就是_modules
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    // 此处创建了一个Vue对象,
    this._watcherVM = new Vue()
    this._makeLocalGettersCache = Object.create(null)

    // 给commit和dispatch绑定this为实例本身,
    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)
    }

    // strict mode
    this.strict = strict

    // 获取_modules根模块的state
    const state = this._modules.root.state

    // 初始化根模块,并且递归初始化所有子模块
    installModule(this, state, [], this._modules.root)

    // 初始化数据响应式系统,使所有state都可以动态响应
    resetStoreVM(this, state)
  }
  
  //
  get state () {
    return this._vm._data.$$state
  }
  
  // 根据type从_mutations找到mutation方法,执行时传入payload
  // 看后续代码需要注意_mutations是怎么来的
  commit (_type, _payload, _options) {
    // 将对象格式的入参转为字符串形式
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    const entry = this._mutations[type]
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
  }

  // 根据type从_actions找到action方法,执行时传入payload,最后处理成Promise返回
  // 看后续代码需要注意_actions是怎么来的
  dispatch (_type, _payload) {
    // 将对象格式的入参转为字符串形式
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    const entry = this._actions[type]
    
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    return new Promise((resolve, reject) => {
      result.then(res => {
        resolve(res)
      }, error => {
        reject(error)
      })
    })
  }
  
  // 这是commit的包裹函数,在执行commit之前会把_committing改为true,
  // 之后再修改回来
  // 在开启严格模式下,如果发现state变化但是_committing不为true,
  // 就说明不是通过commit修改state,将会告警报错
  _withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }
}

看到这里,我们发现有三个地方一定是非常重要的,分别是

this._modules = new ModuleCollection(options)

installModule(this, state, [], this._modules.root)

resetStoreVM(this, state)

我们把这三个地方搞清楚了,基本上就理解了vuex的工作原理与设计。

3.5 ModuleCollection类

说明:

  1. path格式是这样的:

[]代表根模块

['user']代表获取user模块

['user','privateUser']代表获取privateUser模块。

  1. forEachValue用来遍历对象的键值对,回调函数的参数分别代表对象的值和对象的键

ModuleCollection构造函数仅执行一个注册函数register,那么注册函数就是关键,需要重点看。

js 复制代码
export default class ModuleCollection {
  constructor (rawRootModule) {
    // 注册根模块
    this.register([], rawRootModule, false)
  }

  // 根据路径获取模块
  get (path) {}

  // 用/分割,组成namespace
  getNamespace (path) {}

  // 更新模块
  update (rawRootModule) {}

  // 注册模块
  register (path, rawModule, runtime = true) {
     // 创建Module实例,rawModule其实就是我们创建Store传入的options
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      // 根模块
      this.root = newModule
    } else {
      // 这段代码要在下面的递归执行后才会进入到,将子节点挂到父节点的_children下
      // slice(0, -1)表示从0开始到倒数第二个
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    // 如果配置信息里有包含modules,则进行递归注册,注册完后会挂载到父模块的_children下
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

  // 取消注册
  unregister (path) {}

  // 判断是否注册
  isRegistered (path) {}
}

3.6 Module类

Module的构造函数其实也就是简单的记录信息。

js 复制代码
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)
    this._rawModule = rawModule
    const rawState = rawModule.state
    // 说明state配置可以是函数也可以是对象
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  get namespaced () {}

  addChild (key, module) {
      this._children[key] = module
  }

  removeChild (key) {}

  getChild (key) {}

  hasChild (key) {}

  update (rawModule) {}

  forEachChild (fn) {}

  forEachGetter (fn) {}

  forEachAction (fn) {}

  forEachMutation (fn) {}
}

所以this._modules = new ModuleCollection(options)这段代码的作用就是创建一个模块树实例,再回头看这张图片可能就更好理解了。

3.7 installModule

接下来看看注册组件方法写了什么内容,传入的参数分别是store的实例、根模块state、代表根模块的路径、根模块实例。

installModule(this, state, [], this._modules.root)

js 复制代码
export function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  // 根据路径获取对应模块的namespace路径,从上到下,用/拼接在一起
  const namespace = store._modules.getNamespace(path)

  // 如果模块开启了namespace
  if (module.namespaced) {
    // 则将路径注册到_modulesNamespaceMap对象中,对应的值是整个模块实例
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    // 递归的时候才会执行到这里,将子模块的state挂到父模块的state下,键为模块名称
    // 从这里可以看出,如果父模块state中定义了和子模块名称一样的变量,会被子模块覆盖掉
    // {
    //  state:{
    //    user:'lsm'
    //  },
    //  modules:{
    //    user:{state:{}}  //这里的user模块会覆盖上面的user,测试环境会有告警
    //    }
    // }
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }
  
  // 生成各个模块对应的上下文,里面包含commit和dispatch方法
  const local = module.context = makeLocalContext(store, namespace, path)

  // 遍历模块的mutations,将其注册到store实例的_mutations中,
  // 可以注册多个同名的mutations
  module.forEachMutation((mutation, key) => {
    // 这里的namespacedType形如 'user/setName',所以我们调用commit的
    // 时候才要这么写 this.$store.commit('user/setName','new name')
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  // 遍历模块的actions,将其注册到store实例的_actions,
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  // 遍历模块的actions,将其注册到store实例的_wrappedGetters,
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // 递归注册子模块
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
js 复制代码
// 构建组件内部上下文,主要是提供了dispatch和commit方法,getters和state
// 如果模块没有开启namespace,则使用根模块上的dispatch和commit方法
function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''

  const local = {
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
      }
      return store.dispatch(type, payload)
    },

    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
      }
      store.commit(type, payload, options)
    }
  }

  // 设置上下文的getters和state,之所以用函数来写,是因为他们会随着state变化
  // 这样每次获取到都是最新的
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}
js 复制代码
// 注册Mutation方法,可以看到同名的mutations方法是可能会有多个,
function registerMutation (store, type, handler, local) {
  // 获取已存在的mutations,在此基础上增加
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    // 给mutations的this绑定store,并且带上state参数
    handler.call(store, local.state, payload)
  })
}

// 注册actions与mutations类似,不同的是第一个参数加了不少内容,
// 写actions时可以直接获取,并且返回值被处理成promise
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    
    return res
   
  })
}

// 注册getters,并且补充了执行时的参数
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

installModule方法中主要做了以下事项:

  1. 将所有模块的state拼接到根模块的的state
  2. 构建了模块的上下文context,包含commit,dispatch,getters,state
  3. 将所有模块的namespace汇总到实例的_modulesNamespaceMap对象
  4. 将所有模块的mutation汇总到实例的_mutations对象
  5. 将所有模块的action汇总到实例的_actions对象
  6. 将所有模块的getters汇总到实例的_mutations对象

上述操作完成之后,我们就可以使用this.$store.[commit,dispatch]这些方法了。

接下来就是利用Vue实现state和getters的响应式更新就行了。

3.8 resetStoreVM

resetStoreVM(this, state) 传入参数:当前实例,也就是store,当前实例的state,前面已经构建好了

该函数执行之后,state和getters就都具备响应性了。

js 复制代码
function resetStoreVM (store, state, hot) {
  const oldVm = store._vm
  
  store.getters = {}
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // 将getters构造成计算属性,并且挂到store的getters对象上
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // 将state挂到vue的data中,这样它就拥有了响应性,如果state变化了
  // 所有使用到它的地方都会自动更新
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  

  // 开启严格模式,state只能通过mutation来修改
  if (store.strict) {
    enableStrictMode(store)
  }
}

3.9 语法糖函数

语法糖函数设计十分巧妙,下面以mapState为例,讲一讲是如何实现的,其他几个实现都是类似的。

首先回顾一下mapState的基本使用方法:

...mapState(['name','locale'])

或者

...mapState('user',['name','age'])

js 复制代码
// 处理参数,将namespace转为带斜杆的形式,
// 这样就可以在_moduleNamespaceMap中拿到对应的module实例
// 另外还有一个作用,如果只传入一个参数,namespace设为'',也就是取根模块
function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

/**
 * 也是参数处理,
 * 数组转换为对象数组([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
 * 对象也转为对象数组({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
 * @param {Array|Object} map
 * @return {Array}
 */
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] }))
}


export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      // 因为这是放在vue的computed中使用,这里的this就是代表vue组件
      let state = this.$store.state
      let getters = this.$store.getters
      if (namespace) {
        // 从_modulesNamespaceMap获取module
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        // 如果有module,则使用module中的state和getters
        state = module.context.state
        getters = module.context.getters
      }
      // val也支持函数的形式
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
  })
  // 最终返回的是一个对象,对象的每个键对应的都是一个函数,
  // 这就是为什么可以使用...来解构mapState
  return res
})

看完源码可以发现,mapState还可以这么写:

...mapState('user/',{aliasName:'name'}) 给state起别名

也可以传入一个函数:

js 复制代码
...mapState('user',{
    desc:(state,getters)=>{
        return `姓名${state.name},年龄:${state.age}`
    }
)

4 结语

本次源码阅读就介绍到这里,如有错误,还请大家指正!

相关推荐
余生H14 分钟前
深入理解HTML页面加载解析和渲染过程(一)
前端·html·渲染
吴敬悦44 分钟前
领导:按规范提交代码conventionalcommit
前端·程序员·前端工程化
ganlanA1 小时前
uniapp+vue 前端防多次点击表单,防误触多次请求方法。
前端·vue.js·uni-app
卓大胖_1 小时前
Next.js 新手容易犯的错误 _ 性能优化与安全实践(6)
前端·javascript·安全
m0_748246351 小时前
Spring Web MVC:功能端点(Functional Endpoints)
前端·spring·mvc
SomeB1oody1 小时前
【Rust自学】6.4. 简单的控制流-if let
开发语言·前端·rust
云只上1 小时前
前端项目 node_modules依赖报错解决记录
前端·npm·node.js
程序员_三木1 小时前
在 Vue3 项目中安装和配置 Three.js
前端·javascript·vue.js·webgl·three.js
lxw18449125141 小时前
vue 基础学习
前端·vue.js·学习
徐_三岁1 小时前
Vue3 Suspense:处理异步渲染过程
前端·javascript·vue.js