从vuex源码分析module与namespaced

使用vue已经有半年有余, 在各类正式非正式项目中用过, 开始专一于业务比较多, 用到如今也碰见很多由于理解不深致使的问题. 有问题就有找缘由的勇气, 因此带着问题搞一波.html

带着问题看源码

因此来整理了一下使用过程当中不注意或者不规范, 或者简化写法的奇技淫巧, 会结合文档的说明和实际的问题来看看源码, 问题:vue

  • module在vuex里实际的数据结构
  • namespaced在vuex里实际的数据结构
  • mapState, mapActions等helper的正确用法(配合module/namespaced), 或者是否存在更多骚用法
  • mutation中赋值/触发state变化原理

源码分析

看的源码版本为vuex2.3.1git

咱们使用vuex多是相似:github

import Vue from 'vue'
import Vuex from 'vuex'
import plugins from './plugins'
Vue.use(Vuex)
export default new Vuex.Store({
    state: {
        todo: ["todo1", "todo2"]
    },
    mutations: {
        mutationName(state, payload) {
            state.xxx = payload.xxx
        }
    },
    actions: {
        actionName({ commit, dispatch }, payload) {
            commit(mutationName, payload)
        }
    },
    modules: {
        catagories: {
            state: {},
            mutations: {}
        }
    },
    plugins
})

使用vuex的方法为使用Vue.use来installvuex, 并new一个Store实例, 咱们来看一下vuex核心对象.vuex

Store对象分析

line6: 本地vue变量, 在install时会被赋值, 以后会经过vue是否为undefined来判断是否installapi

Store对象构建

line10~14: 判断vuex是否被正确使用
line16~26: 获取options, state能够和vue的component的data同样为函数return一个对象, 会在这段代码中被parse
line28~36: store对象内部变量初始化
line39~46: 绑定commit和dispatch方法到自身
line54: 装载动做
line58: 装载响应动做
line61: 调用插件数组

store内部变量初始化

this._committing = false

是否合法更新state的标识, 对象有方法_withCommit是惟一能够改动_committing的方法, 只有对象内部使用_withCommit更新状态才是合法的, 在strict模式下非法更新state会抛出异常.数据结构

this._modules = new ModuleCollection(options)

modules的cache, 直接把store的参数所有扔给了ModuleCollection新建一个modules对象.app

点击跳转ModuleCollection对象来看分析.函数

this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()

其他的变量是新建了空的变量, 以后会在install模块的时候赋值.

绑定dispatch和commit方法

line39~46, 对dispatch和commit方法进行绑定, 使dispatch方法能够调用在Store对象上注册过的._actions

._mutations的方法.

dispatch方法在line108, 先兼容了参数的写法, 取到参数, 而后判断Store对象的_.actions属性是否注册过, 若是注册过多个, 将会依次调用. 也就是若是type重复了也是会调用屡次的, 这个地方若是出错debug会很是困难. 暂时没有理解vuex此处设计的意图.

commit方法稍微多一点, 大致思路是同样的, 只是直接执行没有返回值, dispatch会返回执行结果. 另外在line95进行了subscriber的操做, 咱们暂且不知道subscriber的做用. 稍后再看.

install模块

首先来看参数:

function installModule (store, rootState, path, module, hot)
// 调用
installModule(this, state, [], this._modules.root)

line255 根据path得到namespace, 作法是读取path的每一个模块, 若是namespaced为true则拼接, 例如path为['catagories', 'price', 'detail'], 其中price的namespaced为false, 其他为true, 那么得到的namespace为catagories/detail/.

line258~260 把namespaced为true的module注册到_modulesNamespaceMap.

line271makeLocalContext函数整理了namespace和type的关系. 在以后的三个module.forEachXxx中, 都调用了registerXxx, 最后的参数都是makeLocalContext的返回值. 咱们来分析一下makeLocalContext的做用:

被注册到全局的mutation/actiongetter实际的type相似于namespace1/namespace2/type的形式, 而咱们在namespaced为true的module中调用的type只是:type. 因此在namespace[true]的action中调用的全部dispatch, commit, getter, state 都会被加上 path.join("/") + "/" 的type来调用到正确的方法.

根据注册的type, 我还获得了一个偏门结论: 能够经过设置type为namespace1/namespace2/type来调用其余namespace的type(待测试), 由于他们是这样被注册的.

install child module

经过比较, install child module的时候是改了第三第四个参数: path => path.concat(key), module => module.getChild(key).

主要区别只是在line264~268, 与ModuleCollection的递归注册子module行为相似, 递归的path参数流程上只是多了一步把当前loop产生的对象挂到父节点上. 作法也是同样的, 把module名字(path)做为key, 套在父级state上. 也就是结构为:

state: {
  ...currentState,
  moduleName: {
    ...subState
  },
  module2Name: {
    ...anotherSubState
  }
}

在以前注册Mutation的时候vuex也是经过这个方法来试mutation得到嵌套过的state做为arguments[0]的.

Store对象总结

store对象把传入的options放入了各个变量进行储存, 并提供了commit, dispatch等方法来调用和处理他们:

._modules

这里存放raw的modules, 未经处理的, 以module名字做为key的方式递归子module.

.state

这里也是以module名字做为key的方式递归储存传入的state

entrys

这里的entry指._actions, ._mutations, ._getters. 他们的储存方式并无递归储存key, 而是平级的, 用/来分割namespace来分辨type, 并在注册时把当前的entry绑定对应的state(经过getNestedState方法).

问题: 若是在不一样module注册了相同type的mutation, 会发生什么?

回答: 会依次在本身的state中执行, 不会影响对方state, 可是会形成错误执行. (待测试). 因此应该在大的项目中尽可能使用namespaced[true]的方式, 而不是命名的方式.(可是也是能够利用/来串namespace的, 因此本身type命名避免/)

._modulesNamespaceMap

根据namespace为key来存放子module

初始化Store VM

这里会新建一个Vue实例并赋值给Store对象的._vm属性, 把整个vuex的状态放进去. 并判断严格模式, 若是为严格模式会在非法改变状态的时候抛出异常.

这样整个构建动做已经完成了, 那么这个._vm在何时用的, 请看下面的章节.

Store对象的属性&方法

line64 state的getter方法, 会获取._vm的vue实例的state. 因此咱们在vue代码中this.$store.state.xxx获取到的东西就是这个vue的实例的数据.

line68 当直接set Store的state时报错, 只能经过设置._vm来进行.

剩余的方法的是vuex的进阶用法, 是能够在使用时对vuex状态进行操做的方法, 详见文档

ModuleCollection对象

咱们来看下ModuleCollection的构造方法.

register根module

调用了register方法, 把参数的path设为根目录, runtime设为false.

register方法一开始(l30)就判断了除state外的属性的值是否为函数, 若不是则抛出异常.

line33 把module参数(仍是初始的options, 就是{state:{...}, mutations:{...}, actions: {...}}这个)和runtime = false 来构建了Module对象(稍后咱们看Module对象的构造)

line35 把ModuleCollection的root私有变量设为了刚才使用初始options新建的Module对象.

line42 若是初始options有modules这个属性, 就开始递归注册modules.

递归register子module

上面是register的第一个参数path为空, 也就是root节点的时候的流程, 在最后一部分(line42)根据是否当前注册的module含有modules属性来递归注册, 这部分咱们来看一下register的path参数的行为会把数据存成什么结构. 以概览部分的例子的参数为例(modules含有一个key为catagories)来走一遍代码流程. (开始~)

被做为子module传入register方法的参数应该为: path(['catagories']), rawModule(state: {},mutations: {}), runtime(false).

注意到的是, 若是catagories有同级module, 被传入的path也是一个元素的数组, 也就是path的意思应该相似于从跟到当前module的层级, 对于兄弟节点是无感的.

这里的runtime还没有明白用途, 多是在别处调用的. 注册流程应该runtime都为false.

一路看下来, 也是new了一个Module对象, 可是没有走到line35把new出的对象放到root变量里, 而是在line37~38去寻找当前module的父节点并把本身做为child, append到父节点上.

这里又脑补了一下数据结构: path.slice(0, -1)是获取被pop()一下的path, path[path.lengt - 1]是获取当前path的最后一个元素, 也就是当前正在被register的module的key. 因此以前对于path的数据结构判断是正确的.

这里的appendChildgetChild很明显是Module对象的方法了, 咱们再继续看Module对象的结构.

Module对象

最后来看Module对象的构造~

接受2个参数, 一个rawModule, 一个runtime, 第一个参数是刚才相对于key为catagories的value, 也就是相似{state: xxx, mutations: xxx, actions: xxx}的options.

Module的构造函数只是把参数拆分, 放入了本身的私有变量, 其中state也接受函数, 并执行函数parse成对象存入私有变量. 其余变量都是原封不动储存的, 因此vuex给他起名为 rawModule 吧. 剩下那些方法都是顾名思义的, 语法上也简单, 没什么好看的.

总结

那么这样Store对象的._modules属性的数据结构已经很清楚了. 相似于(脑内):

{
  // (ModuleCOllection实例)
  root: {
    // (Module实例)
    _rawModule: {
      state: {...}, mutations: {...}, // ...(全是options直接传入)
    },
    state: {}, // 进行过parse的state, 若是是function会调用并赋值
    _children: {
      catagories: {
        // (Module实例)
      },
      anotherModule: {
        // (Module实例), 递归
      }
    }
  }
}

总结一点, 就是这里贮存的数据都是"raw"的.

helpers

全部的helper都用了两个wrap方法, 先来看下这两个方法的做用.

normalizeNamespace

由于helper是都接受两种传参方式:mapState(namespace, map) / mapState(map) , 若是第一个参数为map时这个函数把namespace设为空字符串 , 而且检查namespace的最后一个字符是否是/, 若是不是的话加上.

normalizeMap

咱们map的内容也接受两种语法:

[
  "state1",
  "state2"
]

或者是

{
  state1: state => state.state1,
  state2: state => state.state2
}

这个wrap函数会把两种形式都normalize为含有keyval属性的数组, 便于统一处理. 也就是上面个两个形式会转化为:

// Array like
[{
  key: "state1",
  val: "state1"
}, {
  key: "state2",
  val: "state2"
}]
// Object like
[{
  key: "state1",
  val: state => state.state1
}, {
  key: "state2",
  val: state => state.state2
}]

mapState

这里作了2个处理:

  • 若是namespace不为空, 把state和getter的环境切换到相对于namespace的环境(就是以前的makeLocalContext的返回值)
  • 若是val为函数则执行, 不然返回state的val为键的属性. 二者的执行环境皆为处理过namespace的local环境.

mapActions

mapAction的val语法只接受字符串的, 因此先把val前借namespace, 变为: namespace/val, 这样能符合在Store里注册的entry名.

而后检查了一下namespace是否被注册过, 也就是防碰撞, 而后把val做为type, 并把剩余参数带着dispatch Store里的action.


参考:

原文地址

相关文章
相关标签/搜索