Vuex源码浅读

vuex是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。(Vuex官网

提示

  • 本文只分析主要代码(主流程代码)
  • 阅读本文的前提是至少使用过vuex
  • 源码 版本是 3.x 版本 (截止到2021/04/23)
  • 本文的主要目的是记录一下自己的学习过程

目标

  • 了解Vuex的安装过程
  • 了解Store的实例化过程

先看一个简单的 vuex 是初始化的过程

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

Vue.use(Vuex)
const store = new Vuex.Store({
  state: {
    count: 1
  },
  getter: {
    count: state => state.count
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    increment(context) {
      return Promise((resolve, reject) => {
        setTimeout(() => {
          context.commit('increment')
          resolve(context.state)
        }, 1000)
      })
    }
  },
  modules: {
    a: {
      state: {
        aCount: 1
      }      
    },
    b: {
      state: {
        bCount: 1
      }
    }
  }
})
new Vue({
  el: '#root',
  router,
  store, 
  render: h => h(App)
})
复制代码

Vuex的安装过程

首先 vuex 的安装方法(install)在 src/store.js

// src/store.js
export function install (_Vue) {
  Vue = _Vue
  applyMixin(Vue)
}

// src/mixin.js
function applyMixin (Vue) {
  const version = Number(Vue.version.split('.')[0])
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    ...
	//这里是 vuex1.x版本的代码,可以忽视
  }

  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
    }
  }
}
复制代码

可以看到 vuex 的安装过程还是比较简单的 暴露一个 install 方法,在调用 Vue.use(Vuex) 的时候会调用 install 方法。install方法就是在Vue中mixin一个beforeCreate钩子函数,beforeCreate中干的事就是把通过 new Vuex.Store() 生成的实例绑定到 Vue的每一个实例上 的this.$sotre,这样我们在组件中就可以通过 this.$store 访问 vuex 的实例。

Store的实例化过程

store就是一个数据仓库,为了更方便的管理仓库, 我们把一个大的 store拆成一些 modules,整 个 modules 是一个树型结构。每个 module 又分別定义了 state, getters, mutationsactions 我们也通过递归遍历模块的方式都完成了它们的初始化。

还是在 src/store.js 中,vuex暴露了一个 Store 的构造函数。先看 constructor(),原型方法在执行到的时候再看。

export class Store {
  constructor (options = {}) {
		//...
    const {
      plugins = [],
      strict = false
    } = options

    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()
    this._makeLocalGettersCache = Object.create(null)

    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)
    }

    this.strict = strict
    const state = this._modules.root.state

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

    resetStoreVM(this, state)

    plugins.forEach(plugin => plugin(this))

    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)
    }
  }
  // 下面是原型方法
  // ...
}
复制代码

首先是定义了一大堆初始化变量,大多数我们可以先不看,等到用到的时候再回头看,先看执行代码(干了事的代码)。

分析执行的代码我们可以得到在 constructor() 中主要执行了下面几个方法,其实也就是整个Store 整个初始化的过程中几个主要的过程。

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

下面我们分别进入这几个方法具体分析一下。

new ModuleCollection

代码位置 src/module/module-collection.js

根据构造函数名我们大致可以得出这是初始化storemodules的构造函数,带着这个思路我们还是先看 constructor()

export default class ModuleCollection {
  // rawRootModule 就是我们初始化时传入 new Vuex.Store(optionss) 的 options
  constructor (rawRootModule) {
    this.register([], rawRootModule, false)
  }
}
复制代码

constructor() 很简单,就执行了一个 this.register([], rawRootModule, false)。接着看 register 方法。

rawModule 就是我们传入的 options 或者 options下的 module,也就是用于生成 module 的原始数据。

  register (path, rawModule, runtime = true) {
    // 初始化 module
    const newModule = new Module(rawModule, runtime)
    // 第一次进来 path为空 register 中第一个参数为 []
    if (path.length === 0) {
      // 将通过第一次传入的 options 生成的 modeule 作为 root 绑定到 this 上,方便取 root module
      this.root = newModule
    } else {
      // 之后再进来 path 就有值了 取出来 root module,再次生成的 module 都会绑定到 root module 中
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }
		// 如果 我们初始化的数据中有 modules 就继续递归执行 register
    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
    // 初始化子 module,当前 module 下的子 rawModule 都添加到 _children 中
    this._children = Object.create(null)
    this._rawModule = rawModule
    const rawState = rawModule.state
		// 将moudle中的 state 绑定到 this 上
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
复制代码

这样我们就能生成一个 modules 的树状数据,并把数据放到 this._modules 中。

modules 大概是这个样子,可以看到 modules 目前只是生成了一个树状结构和初始化了每个modulestate,接下来我们继续看代码。

// rowModule 就是该module的原始数据
{
  root: {
    state: { count: 1 },
    _children: {
      a: {
        state: { aCount: 1},
        _children: {}
        _rawModule: rowModule
      },
      b: {
        state: { bCount: 1},
        _children: {}
        _rawModule: rowModule
      }
    },
    _rawModule: rowModule
  }
}
复制代码

installModule

installModule 是初始化 modulegetter mutations actions

直接看代码

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  // 获取当前module 的 namespace,root module的namespace为 ""。
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    // 记录namespace和 module 的对应关系
    // {
    //   'a/': aModule,
    //   'b/': bModule
    // }
    store._modulesNamespaceMap[namespace] = module
  }
  
	// 这里可以先不看,和响应式数据有关的
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    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)
    })
  }

  const local = module.context = makeLocalContext(store, namespace, path)

  // 将 mutation 循环绑定到当前module上
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  // 将 action 循环绑定到当前module上
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })
  // 将 getter 循环绑定到当前module上
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
  // 如果改 module 下有子module 递归继续执行 installModule
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
复制代码

上面的核心代码:

  • const local = module.context = makeLocalContext(store, namespace, path)

  • registerMutation(store, namespacedType, mutation, local)

  • registerAction(store, type, handler, local)

  • registerGetter(store, namespacedType, getter, local)

makeLocalContext

// 我直接把非关键代码直接删除了
function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''
  const local = {
    // unifyObjectStyle 是统一参数的方法,这个方法让 dispatch 和 commit 可以采用两种不同的入参方式
    // commit('increment', 1) | commit({type: 'increment', payload: 1})
    
    // 这里的dispatch
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args
      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
      store.commit(type, payload, options)
    }
  }
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}
复制代码

可以看到 makeLocalContext 返回了一个 local对象,local对象的目的是为该module设置局部的 dispatchcommit方法以及gettersstate(由于namespace的存在需要做兼容处理)

定义local环境后,循环注册该module下的 mutations actions getter

registerMutation

注册 mutation

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})

function registerMutation (store, type, handler, local) {
  // 通过 namespace 找到当前模块的 mutations
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // 循环注册mutations
  entry.push(function wrappedMutationHandler (payload) {
    // 将 store 绑定到每一个 mutation 的 this 上 然后把 local.state 作为第一个参数传入到每一个mutation中,这也是为什么我们在使用commit的时候第一个参数为当前 module 的 state
    // commit实际调用的不是我们传入的handler,而是经过封装的
    handler.call(store, local.state, payload)
  })
}
复制代码

registerAction

注册 action 的过程和 注册 mutation 的过程差不多,需要注意的是 action 被重新封装后 会返回一个 Promise 对象。

module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key
  // 兼容写法
  const handler = action.handler || action
  registerAction(store, type, handler, local)
})

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload) {
    // 传入 dispatch commit 等参数供我们调用 action 时作为第一个参数使用
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // 使用 action 需要 浏览器支持 Promise
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}
复制代码

registerGetter

getters 处理同理

module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})

function registerGetter (store, type, rawGetter, local) {
  // getters只允许存在一个处理函数,getter重复需要报错
  if (store._wrappedGetters[type]) {
    if (__DEV__) {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  // 存储封装后的getters处理函数
  store._wrappedGetters[type] = function wrappedGetter (store) {
    //  传入 state getters 等参数供我们调用 getters 时作为第一个参数使用
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}
复制代码

这样我们就给 store 注册了 actions mutations getters

resetStoreVM

这一步主要就是让 store 中的 stategetter 实现响应的一步,把 stategetter 都绑定到一个 Vue 的实例上实现数据的响应式。

function resetStoreVM (store, state, hot) {
  // 缓存之前的 vue 实例
  const oldVm = store._vm

  store.getters = {}
 
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {

    computed[key] = partial(fn, store)
    // 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的computed属性
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })


  const silent = Vue.config.silent
  Vue.config.silent = true
  // 这里 new Vue() 并把 state 和 getter 绑定到实例上 实现响应式
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    // 解除旧的vue实例对state的引用,并销毁旧的Vue对象
    if (hot) {
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}
复制代码

简单来说就是通过 new Vue()stategetter 绑定到 Vue 的实例上 实现响应式,然后吧 Vue 的实例绑定到 store_vm 属性上。

到这里 Store的实例化过程的主流程就分析完毕了。

最后来看看我们最常用到的 commitdispatch 方法

commit

  commit (_type, _payload, _options) {
    // 处理不同的传参方式
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    // 找到对应的 mutation 方法
    const entry = this._mutations[type]
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    // 通过 _withCommit 来执行 mutation
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
	  // 订阅者函数遍历执行,传入当前的mutation对象和当前的state
    this._subscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .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'
      )
    }
  }
复制代码

_withCommit

_withCommit (fn) {
  const committing = this._committing
  // 只有通过 _withCommit 执行 mutation '_committing' 才为 true
  this._committing = true
  fn()
  // 执行完毕 _committing 重新置为 false
  // 这也是为什么 mutation 必须为 同步函数的原因,只有为同步函数才能正确的等待 mutatuion执行完毕后修改 _committing 的值
  this._committing = committing
}

// 监听对state的修改,若 _committing 不为 true 则抛出警告
function enableStrictMode (store) {
  // this._data.$$state 指向 store 的 state
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (__DEV__) {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}
复制代码

dispatch

dispatch (_type, _payload) {
    console.log(this, 'this')
    // check object-style dispatch
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    // 找到对应的 action 方法
    const entry = this._actions[type]
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    try {
      this._actionSubscribers
        .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
        .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)
      }
    }

    // 执行 action 并返回一个 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)
      })
    })
  }
复制代码

最后

文章只介绍了 vuex 的主流程代码,源码中还有一些辅助函数、工具函数类似mapStatemapGettersmapActionsmapMutations等等,有兴趣的可以打开源码看看。

第一次写文章,如果大家发现有错误或者疑问,再或者哪里没写明白的地方,欢迎评论指出。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享