简易分析下vuex源码

417 阅读4分钟

前言

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。他维护着一个对象,所有的属性都是相应式的。更新任意属性都触发依赖组件发相应式更新。Vuex中只有commit 方式可以修改状态,是一个单项数据流模式。

使用


import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

new Vue({
  el: '#app',
  store,
})


// 想要修改
methods: {
  increment() {
    this.$store.commit('increment') // 必须要触发commit修改state
    console.log(this.$store.state.count)
  }
}

代码结构

│  helpers.js     // 提供mapSate,mapMutations,mapActions,mapGetters 等操作api函数
│  index.cjs.js
│  index.js     // 入口文件
│  index.mjs
│  mixin.js   // 混入方法,提供将store 仓库挂载到每个vue实例的方法
│  store.js   // 整个代码核心,创建store对象
│  test.txt
│  util.js   // 工具类
│  
├─module // 模块化module-collection.js //创建模块树module.js
│      
└─plugins   // 插件
        devtool.js // 开发环境工具插件,针对chorme安装 vue-tool 插件 
        logger.js // state状态改变日志查询

代码分析

首先我们想要使用vuex必须要先安装一下vue.use(vuex) vue会执行一个install方法。我们来看一下install方法是怎么实现的。

代码调试

首先我们来看一下examples这个文件夹打开时候会看到一个webpack配置文件webpack.config.js,在这个配置文件中添加devtool

devtool: 'source-map',

然后执行dev打包 打开examples准备的例子,如:http://localhost:8080/chat/

安装

src\store.js

export function install (_Vue) {
// 如果已经安装过报错,只能安装一次
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  // 安装vuex
  applyMixin(Vue)
}


// 在Store这个类中有这么一段代码
// 通过script标签这种方式引入会自动安装
 if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }
// 防止重复安装
    if (__DEV__) {
      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.`)
    }

在vuex和vuerouter都中采用了通过传递参数来或者vue实例没有通过引入的方式,这样的好处是打包的时候不会把vue打包进去使得整个体积变大

applyMixin

src\mixin.js

export default function (Vue) {
// 获取vue版本
  const version = Number(Vue.version.split('.')[0])
// 在vue2版本中使用混入的方法
// 全局混入beforeCreate
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
  // 兼容vue1版本
  // 获取保存 _init方法 在vue中初始化要走这个方法
  //重写init方法,配置项传入vuexinit然后再执行刚才保存的_init方法。
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  function vuexInit () {
  // 在vue中执行this是vue实例
    const options = this.$options
    // 这个就是为什么在new vue({store})要传入stopre 会检查当前组件或则根组件有没有这个选项然后挂载store实例 给this.$store
    if (options.store) { //根组件
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) { //向上传递$store,保证任何组件都可以访问到
      this.$store = options.parent.$store
    }
  }
}

根据vue1、vue2不同的版本,vue1使用重写_init方法(注:_init方法是vue是初始化执行的方法),vue2使用全局混入beforeCreate生命周期执行vuexInit,然后挂载store实例,子组件引用父组件的store层层递归,能让我们在任意组件上都可以访问到store实例,并且store实例只实例化了一次。

初始化

接下来我们看一下store的创建

src\store.js

//最后传递给vue的是一个store的实例
//由于导出的时候是export default {Store}
//所以我们实例化的时候要实例Vuex的Store
//let store = new Vuex.Store({}) 

export class Store {
  constructor (options = {}) {
      const {
      plugins = [], // 初始化插件,这个选项每次暴露出mutation ,接收唯一的参数store 
      strict = false // 是否开启严格模式,开始了严格模式只要不是commit触发状态发生改变就抛出错误
    } = options 
  }
    // 做一系列的初始化 所有都是私有属性
    this._committing = false //是否在提交状态
    this._actions = Object.create(null) // actions集合
    this._actionSubscribers = [] //action订阅者集合
    this._mutations = Object.create(null) // mutations集合
    this._wrappedGetters = Object.create(null) // getters集合
    this._modules = new ModuleCollection(options) // modules集合
    this._modulesNamespaceMap = Object.create(null) // 命名空间集合
    this._subscribers = [] // 监听队列,当执行commit时会执行队列中的函数
    this._watcherVM = new Vue() // vue实例,vue组件用于watch监听变化 类似eventbus
    this._makeLocalGettersCache = Object.create(null)
  
}

下面我们来看一下this._modules

src\module\module-collection.js


export default class ModuleCollection {
  constructor (rawRootModule) {
    // 接收整个option作为参数 这个option就是我们实例化的时候的参数
    // {store:xxx,mutaions:xxx,actions:xxx.modules:xxx}
    // 执行注册函数 注册根模块
    this.register([], rawRootModule, false)
  }

  register (path, rawModule, runtime = true) {
    if (__DEV__) {
      assertRawModule(path, rawModule)
    }
    // 初始化Module
    const newModule = new Module(rawModule, runtime)
    // 对于第一次初始化 ModuleCollection 时
    // 会走第一个 if 条件,因为当前是 root
    if (path.length === 0) {
      this.root = newModule
    } else {
    //子元素把关系添加到父元素,形式关系树
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // 递归注册
    /*
   
    const moduleA = {
        state: { ... },
        mutations: { ... },
        actions: { ... },
        getters: { ... }
    } 
    const moduleB = {
      state: { ... },
      mutations: { ... },
      actions: { ... }
    }
    const store = new Vuex.Store({
          state: { ... },
          modules: {
            a: moduleA,
            b: moduleB
          }
        })
     */
    // 把每个modules都注册一边
    // 递归注册
    
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

src\module\module.js

export default class Module {
  constructor (rawModule, runtime) {
  //缓存运行时的标志
    this.runtime = runtime
    //创建一个空对象来保存子模块
    // 缓存children
    this._children = Object.create(null)
   // 缓存初始化的rawModule
    this._rawModule = rawModule
    const rawState = rawModule.state

    // 赋值state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
  //下面是 获取namespaced、对_children增删改查、跟新rawModule、遍历_children、getter、action、mutation
  get namespaced () {
    return !!this._rawModule.namespaced
  }
  
  addChild (key, module) {
    this._children[key] = module
  }
 
  removeChild (key) {
    delete this._children[key]
  }

  getChild (key) {
    return this._children[key]
  }

  hasChild (key) {
    return key in this._children
  }

  update (rawModule) {
    this._rawModule.namespaced = rawModule.namespaced
    if (rawModule.actions) {
      this._rawModule.actions = rawModule.actions
    }
    if (rawModule.mutations) {
      this._rawModule.mutations = rawModule.mutations
    }
    if (rawModule.getters) {
      this._rawModule.getters = rawModule.getters
    }
  }

  forEachChild (fn) {
    forEachValue(this._children, fn)
  }

  forEachGetter (fn) {
    if (this._rawModule.getters) {
      forEachValue(this._rawModule.getters, fn)
    }
  }

  forEachAction (fn) {
    if (this._rawModule.actions) {
      forEachValue(this._rawModule.actions, fn)
    }
  }

  forEachMutation (fn) {
    if (this._rawModule.mutations) {
      forEachValue(this._rawModule.mutations, fn)
    }
  }
}

ModuleCollection主要将初始化传入的配置项构造为一个module对象,并递归调用注册,形成父子级

下面回到store.js初始化完毕之后开始安装模块

src\store.js

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




function installModule (store, rootState, path, module, hot) {
 // 判断是否是根modules
  const isRoot = !path.length
  // 获取namespace
  // 对于 modules: {a: moduleA} 来说
  // namespace = 'a/
  const namespace = store._modules.getNamespace(path)

  // 查重、向modulesNamespaceMap集合添加module key为namespace
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
  }

// 设置 state
// 不是根组件 没有设置preserveState:是否要保留state
  if (!isRoot && !hot) {
  // 获取父级state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 得到moduleName 
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      if (__DEV__) {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          )
        }
      }
      Vue.set(parentState, moduleName, module.state)
    })
  }
  // 重写dispatch和commit
  const local = module.context = makeLocalContext(store, namespace, path)
  // 注册模块中的 mutation、action 和 getter
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })
  // 这里会生成一个 _wrappedGetters 属性
  // 用于缓存 getter,便于下次使用
  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)
  })
}

安装完成之后会调用resetStoreVM,我们来看下这个方法干了些什么

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

 // 设置getters
  store.getters = {}
  // 初始化getter缓存
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
   // 遍历 _wrappedGetters
  forEachValue(wrappedGetters, (fn, key) => {
     // 给 computed 添加属性
    computed[key] = partial(fn, store)
    // 重写gettrs的get方法  store.getters.xx 实际上是访问的store._vm[xx]
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true 
    })
  })

  const silent = Vue.config.silent
  // 暂时将Vue设为静默模式,避免报出用户加载的某些插件触发的警告
  Vue.config.silent = true
   // 设置新的storeVm,将当前初始化的state以及getters作为computed属性
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
   // 恢复Vue的模式
  Vue.config.silent = silent

  // 如果开启了严格模式禁止commit以外的方式修改state
  // 使用了store._vm中的watch方法深度监听
  if (store.strict) {
    enableStrictMode(store)
  }

// 若不是初始化过程执行的该方法,将旧的组件state设置为null,强制更新所有监听者(watchers),待更新生效,DOM更新完成后,执行vm组件的destroy方法进行销毁,减少内存的占用
  if (oldVm) {
    if (hot) {
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

Api

commit

vuex中要想改变state只能调用commit方法

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

  set state (v) {
    if (__DEV__) {
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }
// 直接获取state返回我们刚才创建的vue实例中的$$state 实现了相应式,如果要修改就抛出错误

// 下面我们来看一下commit方法
// 接收三个参数:mutation 的类型、参数和另外配置项
commit (_type, _payload, _options) {
// 检查参数,对commit多种形式传参经行处理
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    // 在_mutations集合中找到具体调用的方法
    const entry = this._mutations[type]
    // 如果没有找到抛出错误
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    
    // 设置_committing 回调完成之前是true 完成之后是false
    // handle 就是 wrappedMutationHandler 函数
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })

    // 先拷贝一下,执行订阅函数
    this._subscribers
      .slice() 
      .forEach(sub => sub(mutation, this.state))

    if (
      __DEV__ &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }

dispatch

如果想要使用异步方式去改变state,就需要调用dispatch,然后再通过调用commit来实现异步修改状态

 dispatch (_type, _payload) {
   // 和commit一样检查参数 然后冲actions中找到具体的方法
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    const entry = this._actions[type]
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    try {
      this._actionSubscribers
        .slice()
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (__DEV__) {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }
// 因为dispatch支持异步,所以使用Promise.all来执行异步函数并且判断所有异步函数是否都已经执行完成,所以在文件最开始判断了当前环境必须支持promise就是这个原因
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
    return new Promise((resolve, reject) => {
      result.then(res => {
        try {
          this._actionSubscribers
            .filter(sub => sub.after)
            .forEach(sub => sub.after(action, this.state))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in after action subscribers: `)
            console.error(e)
          }
        }
        resolve(res)
      }, error => {
        try {
          this._actionSubscribers
            .filter(sub => sub.error)
            .forEach(sub => sub.error(action, this.state, error))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in error action subscribers: `)
            console.error(e)
          }
        }
        reject(error)
      })
    })
  }

mapState

src\helpers.js

export const mapState =
// 如果有namespace查看没有有/ 没有加上 
// 如果没有namespace当作一个匿名 参数转化一下
normalizeNamespace((namespace, states) => {
  const res = {}
  // 检测states格式
  if (__DEV__ && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }
  // 格式化states 形成[{ key, val: key }]这样形式的数组
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      let state = this.$store.state
      let getters = this.$store.getters
      // 如果有命名空间则去模块中去找
      if (namespace) {
        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]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

mapGetters的逻辑跟mapState类似 在mapActions和mapMutations中会返回执行dispatch或者commit的结果

总结

  1. state内部支持模块配置和模块嵌套如何实现的?
    答:在store类中有makeLocalContext方法所有的module都会有一个local context,根据path进行匹配
  2. 执行dispatch触发action的时候,只需要传入type,payload,action中第一个参数store从哪获取?
    答:store初始化的时候调用了registerAction然后在处理handler的时候传入
let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
// 传入这些参数
  1. 如果区分是state是外部修改的还是通过调用commit修改的?
    答:在执行commit方法的时候,会调用this._withCommit(fn),这个时候会设置_committing为true,当回调执行完毕之后再次修改为原来的值。enableStrictMode这个方法会创建一个watch,监听state如果_committing不是true修改的会抛出错误。

文章有不对的地方请大佬们指正
最后麻烦大家点一个小赞

附上几个之前写的小文链接

vue1原理分析

vue2源码分析

vuerouter源码分析