Vuex源码解析

Vuex概述

Vuex是一个专为 Vue.js 应用程序开发的状态管理模式,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。在开发当中,运用Vuex统一管理我们的资源,使得开发更明确,适合用在中大型项目。
tips:本文不对Vuex的使用做叙述,只对Vuex源码进行解析,以下跳转Vuex官方中文文档:https://vuex.vuejs.org/zh/

Vuex状态图与源码结构

Vuex状态图
Vuex源码结构

Vuex-install

我们知道,Vue在调用Vue.use()安装插件时,即会调用安装插件的方法。
而在Vuex中,则是调用如下方法:

// 暴露给外部的插件install方法,供Vue.use调用安装插件
export function install(_Vue) {
  if (Vue && _Vue === Vue) {              // 避免重复安装
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 将外界全局vue实例绑定到内部使用的Vue
  Vue = _Vue
  // 根据Vue传入的options ,给Vue实例mixin钩子函数 beforeCreate,实例化 store() 
  applyMixin(Vue)

其中,applyMixin(Vue)会根据Vue的版本,将vuexinit方法mixin到Vue的beforeCreate钩子函数列表中,在Vue实例化的时候调用,从而达到实例化我们的store的目的。而且,在vuexinit方法中,会判断当前vue组件是不是根组件,是,则直接赋值$store,不是,则通过options中的parent获取父组件的store引用,这样每个组件都可以获取到了同一份内存地址的Store实例,于是我们可以在每一个组件中访问全局的Store实例了。

/**
 *  vuexInit会尝试从options中获取store,如果当前组件是根组件(Root节点),则options中会存在store,直接获取赋值给$store即可
 *  如果当前组件非根组件,则通过options中的parent获取父组件的$store引用
 *  所有的组件都获取到了同一份内存地址的Store实例,于是我们可以在每一个组件中通过this.$store访问全局的Store实例了
 */
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })               // 初始化时,store()实例化Store
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    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.
   */

  // Vuex 的钩子函数,会存入每一个Vue实例的钩子列表
  function vuexInit() {
    const options = this.$options
    // new Vue 时有无添加store,有,则store()实例化
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 子组件直接从父组件中获取 $store ,这样就保证了所有组件都公用了全局的一份store
      this.$store = options.parent.$store
    }
  }
}

看完vuex-install,我们再看Store内部定义了一些什么

Vuex class Store

首先,我们看一看构造函数contructor()
构造函数中,初始化了我们要用到的内部变量,并且通过installModule初始化了module,通过resetStoreVM新建一个Vue对象,使用Vue内部的响应式实现注册state以及computed

  /**
   * Store的构造类除了初始化一些内部变量,主要还执行了:
   * installModule:初始化module
   * resetStoreVM:通过VM使得store响应式
   */
  constructor(options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    if (process.env.NODE_ENV !== 'production') {
      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.`)
    }

    const {
      // 插件数组
      plugins = [],
      /*使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。*/
      strict = false
    } = options

    // store internal state
    this._committing = false                // 用来判断严格模式下是否是用mutation修改state的
    this._actions = Object.create(null)     // 存放action
    this._actionSubscribers = []
    this._mutations = Object.create(null)       // 存放mutation
    this._wrappedGetters = Object.create(null)      // 存放getters
    this._modules = new ModuleCollection(options)               // module收集器
    this._modulesNamespaceMap = Object.create(null)           // 根据namespace存放module
    this._subscribers = []                              // 存放订阅者
    this._watcherVM = new Vue()                        // 用以实现Watch的Vue实例
    this._makeLocalGettersCache = Object.create(null)

    // 将dispatch和commit调用的this绑定为store对象本身,否则在组件内部this.dispatch时this会指向组件的vm
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch(type, payload) {           // 向外部暴露的dispatch方法
      return dispatch.call(store, type, payload)                 // 绑定store即this 到type指定的函数上,确保能够调用到this的其他参数
    }
    this.commit = function boundCommit(type, payload, options) {      // 向外部暴露的commit方法
      return commit.call(store, type, payload, options)         // 绑定store即this 到type指定的函数上,确保能够调用到this的其他参数
    }

    // strict mode
    this.strict = strict              // 严格模式

    const state = this._modules.root.state


    // 初始化根module,这也同时递归注册了所有子module,收集所有module的getter到到_wrappedGetters中去,收集设置action,mutation,
    installModule(this, state, [], this._modules.root)             // this._modules.root代表根module才独有保存的Module对象

    // 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed
    resetStoreVM(this, state)

    // 应用 plugins
    plugins.forEach(plugin => plugin(this))

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

然后我们来看installModule,其主要作用就是为module加上namespace命名空间(如果有),注册mutation,action,getter,同时递归安装所有子module

// 初始化module
function installModule(store, rootState, path, module, hot) {
  const isRoot = !path.length                                   // 判断是否是module的根节点
  const namespace = store._modules.getNamespace(path)        // 获取module的namespace

  // 如果有namespace,则在_modulesNamespaceMap中注册
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module               // 注册
  }

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))       // 获取父级的state
    // module的name
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      if (process.env.NODE_ENV !== 'production') {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          )
        }
      }
      // 将子module设成响应式的
      Vue.set(parentState, moduleName, module.state)
    })
  }

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

  // 遍历注册mutation
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  // 遍历注册action
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  // 遍历注册getter
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // 递归安装子module
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

然后我们看到register注册action和mutation时,巧妙地用call()将作用域绑定到store实例上,并且暴露state等我们需要的内部参数。

// 遍历注册mutation
function registerMutation(store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler(payload) {
    handler.call(store, local.state, payload)
  })
}


// 遍历注册action
function registerAction(store, type, handler, local) {
  // 取出type对应的action
  const entry = store._actions[type] || (store._actions[type] = [])
  // 对action进行一层封装,call()绑定作用域与state,commit等方法,使得我们在dispatch第一个参数中可以获取
  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)
    // action执行结果判断是否是Promise
    if (!isPromise(res)) {
      // 不是Promise对象的时候转换成Promise对象
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      // 存在devtool插件时触发vuex的error给devtool
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

// 遍历注册getter
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) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

接着,再看到resetStoreVM中通过新建Vue对象实现响应式注册state和computed

function resetStoreVM(store, state, hot) {
  // 存放之前的vm对象
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  // 通过Object.defineProperty为每一个getter方法设置get方法,依赖vm响应式
  forEachValue(wrappedGetters, (fn, key) => {
    // 遍历wrappedGetters,绑定get方法,这样我们就可以在组件通过this.$store.getters的方式访问
    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

  const silent = Vue.config.silent
  // Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告
  Vue.config.silent = true
  // 前面设置完get后,这里new一个Vue对象,运用Vue内部的响应式实现注册state和computed
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  // 使用严格模式,保证修改store只能通过mutation
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    // 解除旧vm的state引用,以及销毁旧的Vue对象
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null            // 解除旧的state引用
      })
    }
    Vue.nextTick(() => oldVm.$destroy())       // 销毁旧的Vue对象
  }
}

同时,我们都知道Vuex支持严格模式,即只能通过mutation修改vuex状态,他的原理很简单,是通过store实例的vm实例观察state,并判断_committing,而在commit调用方法前,修改_committing的值以达到监视效果,两部分的源码如下:

// Vuex执行严格模式下,$watch观察state,保证所有state的操作必须通过mutation实现
function enableStrictMode(store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      // 检查store中的_committing的值,如果是false代表不是通过mutations的方法修改的
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })         // 深度监视
}
  // 在调用fn()前,修改this._committing,使得严格模式下内部监听state起作用      --function enableStrictMode
  _withCommit(fn) {                         // 调用commit里对应的 fn()
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }

接着我们看到Vuex暴露给外部的dispatch和commit方法,供外部vue实例调用mutation和action:

  // 调用mutation的commit方法
  commit(_type, _payload, _options) {
    // 校验参数
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)       // 判定是否为对象,返回原数据

    const mutation = { type, payload }
    // 取出type对应的mutation的方法
    const entry = this._mutations[type]                        // 取出对应的函数
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }

    // 执行取出的方法
    this._withCommit(() => {
      entry.forEach(function commitIterator(handler) {          // 调用
        handler(payload)
      })
    })

    // 通知所有订阅者
    this._subscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .forEach(sub => sub(mutation, this.state))

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


  // 调用action的dispatch方法
  dispatch(_type, _payload) {
    // 校验参数
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)                 // 判定是否为对象,返回原数据

    const action = { type, payload }
    const entry = this._actions[type]                       // 取出对应的函数
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        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 (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }

    // 是数组则通过Promise.all()包装形成一个新的Promise,只有一个则直接返回
    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
    })
  }

除了满足基本使用的dispatch和action,向外暴露的还有:subscribe,watch, registerModule,unregisterModule 等,解析如下

  /**
   * Store给外部提供了一个subscribe方法,用以注册一个订阅函数,会push到Store实例的_subscribers中,同时返回一个从_subscribers中注销该订阅者的方法。
   */
  // 注册一个订阅函数,返回取消订阅的函数
  subscribe(fn) {
    return genericSubscribe(fn, this._subscribers)
  }

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

  // 观察一个getter方法
  watch(getter, cb, options) {
    if (process.env.NODE_ENV !== 'production') {
      assert(typeof getter === 'function', `store.watch only accepts a function.`)
    }
    // _watcherVM是一个Vue实例,直接采用Vue内部的watch特性提供观察数据getter变动的方法
    return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  }

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

  // registerModule用以注册一个动态模块,也就是在store创建以后再注册模块的时候用该接口,内部实现实际上也只有installModule与resetStoreVM两个步骤
  // 注册一个动态module,当业务进行异步加载的时候,可以通过此接口进行注册动态module
  registerModule(path, rawModule, options = {}) {

    // 将path字符串转化成Array
    if (typeof path === 'string') path = [path]

    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
      assert(path.length > 0, 'cannot register the root module by using registerModule.')
    }

    // 注册
    this._modules.register(path, rawModule)
    // 初始化module
    installModule(this, this.state, path, this._modules.get(path), options.preserveState)
    // 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed
    resetStoreVM(this, this.state)
  }

  // 与registerModule对应的方法unregisterModule,动态注销模块。实现方法是先从state中删除模块,然后用resetStore来重制store
  // 注销一个动态module
  unregisterModule(path) {

    // 将path字符串转化成Array
    if (typeof path === 'string') path = [path]

    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
    }

    // 注销
    this._modules.unregister(path)
    this._withCommit(() => {
      // 获取父级的state
      const parentState = getNestedState(this.state, path.slice(0, -1))
      // 从父级中删除模块
      Vue.delete(parentState, path[path.length - 1])
    })
    // 重制store
    resetStore(this)
  }

结尾

可以看到,Vuex的实现原理很多地方用到了Vue的响应式原理,甚至直接在内部注册了Vue实例,其中call()的引用绑定作用域也是用得很妙,但在我觉得,运用箭头函数进行组件调用时的作用域绑定也可以实现功能。

总结不易,希望能帮到大家哦!!!

编者github地址:传送.
也欢迎和我一起讨论学习:
微信号:carfiedfeifei
qq:1073490398

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值