Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

9. vuex源码分析 #9

Closed
funfish opened this issue Nov 5, 2017 · 0 comments
Closed

9. vuex源码分析 #9

funfish opened this issue Nov 5, 2017 · 0 comments
Labels

Comments

@funfish
Copy link
Owner

funfish commented Nov 5, 2017

前言

前文分析了Vue-router,感觉后劲十足,于是开始分析Vuex。在项目上,Vuex也是常客。它可以很好的管理状态,尤其是跨组件的时候,Vue的单向数据流使得子组件无法修改prop,经常用$emit和$on的话组件是要多难看就多难看。当组件切换,数据需要缓存总不能一直依赖于向上级组件emit传递数据吧?如果要更好的管理状态,Vuex是个很好的选择。Vuex代码量较Vue-router少了很多,而且也没有flow的校验机制,看起来更加习惯了。这里介绍的Vuex版本号为2.4.1。

从示例开始

Vue.use(Vuex)
const state = {
  count: 0
}

const mutations = {
  increment (state) {
    state.count++
  },
  decrement (state) {
    state.count--
  }
}

const actions = {
  increment: ({ commit }) => commit('increment'),
  decrement: ({ commit }) => commit('decrement'),
}
// getters are functions
const getters = {
  evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
}
export default new Vuex.Store({
  state,
  getters,
  actions,
  mutations,
})

上面示例基本上包含了最常用的mutations,getters和actions了。可以发现这一切从Vue.use(Vuex)开始的,对Vue.use不熟悉的可以看上一篇的Vuex-router中对Vue.use的介绍。
Vuex中用到了install方法来提供Vuex的使用环境。和Vue-router不同,Vuex的主要代码功能都在store.js文件里面(这对查阅代码友好度明显提到了不少)。install过程里面用到了Vue.mixin,并用到了beforeCreate钩子,使得Vue实例化和组件加载的时候都可以调用到钩子。设计如下:

const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
  Vue.mixin({ beforeCreate: vuexInit })
} else {
  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 () {
  const options = this.$options
  // store injection
  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的版本分别做了处理,本版是2.0.0及以上的都会采用Vue.mixin的方法,而低版本的,则将修改Vue的内部_init方法,来添加$store至根。高级别的版本则采用mixin的方法,同样也是添加this.$store。在Vue-router里面是采用数据劫持的方法,来通知更新,顺便提供this.$router,对于状态管理而言,数据劫持显然是不需要的,仅仅提供入口this.$store就够了,这样为全局提供了访问store对象的方法,可以轻松得使用this.$store.commit, this.$store.state之类的方法。

Store

Store.js里面最主要的就是Store类,这个也是之前提到的this.$store对象。先看看constructor方法:
在构造里面先判断有无使用install方法,没有则intall一下,接着是断言有无安装Vue,是否支持Promise和是否是通过new创建Store的实例。另外在install过程里面还有是否重复安装Vuex的断言,这个场景会发生在已经先使用Vuex了,但是没有用Vue.use(Vuex)来显式安装Vuex,如果再加上Vue.use(Vuex)就会有这样的提示,尤其是在开发环境和生产环境配置中。
Store初始化过程,有this._modules = new ModuleCollection(options),这个_modules就是Store集合的意思了。Vuex有modules的概念,允许对store进行分割形成不同的模块,每个模块都可以有自己的state,getter,mutation和action,甚至还可以嵌套子模块。于是将这些模块包括根模块一起放入modules里面。this._modules的一个重要api就是注册添加一个模块:

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

这里还可以看到this._modules.root就是根模块,并且对于子模块的,还会被添加到父模块parent的_children对象里面;到这里可以发现this.modules.root和原先的store很像,只是单独分离出state,并且将子模块改为了_children关系,并将_rawMoudule赋值为整个传过来模块,同时为this._modules和每个module都添加不少方法,这些方法自然是为后面做准备的。
在谈commit和dispatch方法之前,先看看后面的模块安装和StoreVM的设置

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

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)
  // 命名空间字典的添加
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }
  // 设置state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }
  const local = module.context = makeLocalContext(store, namespace, path)
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  // 下面省略部分是通过module提供的方法分别对action和getter进行registe
  // 以及对子模块modules的遍历式得注册mutation/action/getter

  // ...
}

对于modules而言,官方文档有介绍到,模块内部的 action,mutation和getter是注册在全局命名空间的,如果想要独立的空间,比如有命名重复的情况下,可以使用namespaced: true来注册单独的空间;同时访问的时候也也要加上模块的名字,否则否则无法定位到。
接着看state的设置,对于if条件语句,若是子模块并且非hot,会获取子模块的亲父级模块,并通过Vue.set方法将该子模块的state添加到亲父模块state里面,这是响应式的,会被Vue劫持到。后面部分就是对action/getter/mutation的注册添加了,这部分后面在讲。

后面是resetStoreVM:

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm
  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })
  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
  if (store.strict) {
    enableStrictMode(store)
  }
  // 如果存在oldVM对其进行销毁
  // ... 
}

刚看到这里时候可能会惊奇何时来的_vm?事实上这个_vm正是这里的核心,_vm是个Vue实例,并将_vm.data.$$state指向的option中的state。细心的话还可以发现在Store类中,其中的Store.state:如下

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

state返回的正是_vm.data.$$state,这个也就是平时所用的this.$store.state。观察resetStoreVM还可以发现通过遍历wrappedGetters,来将wrappedGetters中的方法通过_vm.computed的形式添加到store.getters里面,这么复杂的办法有什么好处呢?而且为什么只是专门处理getter,没有对mutation和action进行这样的处理?getter的方法是对state进行处理提取过滤,而computed是依赖于data的,当data更新的时候computed就自动计算,同样这里也是的,当state更新的时候,通过computed的方法,getter不就自动计算更新了吗?只是这样就有点麻烦。。。。。要新建一个Vue实例,关于_vm,更多的可以点这里

commit和dispatch

在介绍之前先看看前面忽略的,在installModule方法里面对mutation/getter/action等方法的添加机制。
对于registerMutation:

function registerMutation (store, type, handler, local) {
  // 内部的_mutations[type]保存对应的mutattion方法
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    // 在mutation方法里面传入local.state和payload,
    // wrappedMutationHandler只需要payload,符合commit时,仅需传入type和payload
    handler.call(store, local.state, payload)
  })
}

上面方法添加了store._mutations[type],而handler传参里面的local.state又是什么呢?回头看可以发现这里调用了makeLocalContext,生产local变量,makeLocalContext代码这里就不贴出来了。local.state就是对应path的state变量,只是是通过数据劫持的方法获得的,代码中说明getters和state对象都必须要懒加载,因为可能被vm更新影响到,这里是不是指_vm重新创建的时候造成的影响呢?由于namespaced的问题,local里面的dispatch和commit都做了特别处理,但是还是使用store的dispatch和commit的方法,只是传参做了修改。
对于registerAction,类似与mutation,采用了store._actions[type]来保存handler数组,但由于action有用于异步的情况,所以若返回的action不是Promise类型,则进行Promise包装。同时action的传参不是local.state,而是传入local的本身的所有字段和store的getters以及state,这也符合action的基本应用。
对于registerGetter,这里比较简单直接采用store._wrappedGetters[type] = handler的形式,而registerMutation是采用数组的形式。所以对于重复名字的getter就会有告警``[vuex] duplicate getter key: ${type}`。

回到commit方法和dispatch,在Store类构造的时候,有如下:

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

这里面定义commit方法和dispatch方法,这两个就是$store.commit$store.dispatch,而commit这个方法处理起来也是比较简单,就是将_mutations里面对应方法名都执行一遍,并传递payload进去。同时还将_subscribers里面的函数都遍历执行。_subscribers是通过subscribe这个api添加进来:

subscribe (fn) {
  const subs = this._subscribers
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

该方法可以添加订阅函数,每当mutation执行的时候,所有订阅函数都会执行,值得一提的时候在devtool.js文件里面用到了:

store.subscribe((mutation, state) => {
  devtoolHook.emit('vuex:mutation', mutation, state)
})

当使用devtoolHook的时候(这个也涉及到Vue官方推荐的浏览器插件工具Vue devtools)能在每个mutation动作结束后,触发vuex:mutation事件,并在devtools插件内打印动作
还可以看出这个subscribe设计很巧妙,subscribe直接运行是添加订阅函数,而其返回函数就是disSubscribe,就是将订阅函数去除掉,由于不常用,所以就没有直接给出api了,厉害的很。

dispatch该动作类似的,也是调用之前存在_actions里的handlers,只是由于handles可能有多个,并且是异步的原因,若是多个的话需要用Promise.all来执行;

其他Api

日常用的比较多的是registerModule/unregisterModule,两个过程是类似的,注册新模块的时候需要重新installModule和resetStoreVM,这个时候就会将老的_vm delete掉,重新实例化Vue给到_vm。
mapState/mapMutations/mapGetters/mapActions等api结构类似。以mapState为例子:

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  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
})

normalizeNamespace来调整参数,再通过normalizeMap将传入的state调整为{ key, val: key }结构,并根据情况返回。这几个api还是很容易懂的。

结束

一周下来写了两篇源码分析,Vuex的代码和Vue-router相比还是很良心的,没有Vue-router里面那么多弯弯绕绕,Vuex简单明了多了。

@funfish funfish added the Vue label Nov 27, 2017
@funfish funfish changed the title vuex源码分析 9. vuex源码分析 Mar 11, 2018
@funfish funfish closed this as completed Feb 9, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant