vuex

vuexvue-router一样,也是通过插件的方式进行引入的。因为开发中,一般都会使用modules功能将store进行模块拆分,所以例子就通过这种方式进行。

Vue.use(Vuex)

const moduleA = {
  ...
}
const moduleB = {
  ...
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
const vm = new Vue({
  el: '#app',
  render (h) {
    return h(App)
  },
  store
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

我们先看index.js,这个很简单,导出了相关方法,所以关于定义只需要看store.js就好。在install方法中执行了applyMixin,其中就是执行了Vue.mixin({ beforeCreate: vuexInit })。 而vuexInit方法,就是将传入的store实例赋值给了this.$store

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
  }
}
1
2
3
4
5
6
7
8
9
10
11

注册流程就这样非常简单

实例化

Store构造函数中,主要是执行options的解析,也就是执行this._modules = new ModuleCollection(options),实例化ModuleCollection执行了this.register([], rawRootModule, false) 我们来看看register方法。

register (path, rawModule, runtime = true) {
  if (__DEV__) {
    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)
  }

  if (rawModule.modules) {
    forEachValue(rawModule.modules, (rawChildModule, key) => {
      this.register(path.concat(key), rawChildModule, runtime)
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

该方法首先将options通过new Module存放到root,相当于给options存了一个副本。如果用户使用了module就通过循环再次注册这时候会吧module放到_children下面 这时候root的值为

this.root = {
  state: {},
  _children: {
    a: Module,
    b: Module
  },
  _rawModule: {
    modules: {
      a: {},
      b: {}
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

之后就是核心installModule,这个方法主要注册了mutation、action、getter、module.

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set 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)
    })
  }
  // 为了保证不同module可以定义相当名字的函数,vuex给函数名加上了namespaced
  const local = (module.context = makeLocalContext(store, namespace, path))

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

  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)
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

local

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

local的核心是根据namespace定义了dispatchcommit方法。并给gettersstate属性做了响应式处理。

state 初始化

state的初始化过程中,首次没什么好说的,这个主要作用在于处理Module,这时候!!root!hot都为true。如果在options中定义了state那么 parentState就是它的值,_withCommit是一个锁,当有回调函数在执行的时候,别的回调函数就无法执行。在回调函数中调用了Vue.set,将模块中的属性放到了 主模块中,并且用set使其响应式

const parentState = {
  d: 0
}
// 设置A模块的state
Vue.set(parentState, 'a', { count: 0 })
// 设置B模块的state
Vue.set(parentState, 'b', { count: 0 })
1
2
3
4
5
6
7

通过namespace的方法,即使模块中属性名字相同也不会冲突。

更重要的是我们知道,state修改会触发组件重新渲染,是响应式的。其实关于响应式的定义,在resetStoreVM方法中

store._vm = new Vue({
  data: {
    $state: state
  },
})
1
2
3
4
5

这个传入的state就是初始化好的state

Mutation 初始化

 module.forEachMutation(function (mutation, key) {
  var namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})
1
2
3
4

看上面这段代码,通过module的循环,拿到key,然后组装成namespacedType,对于上面的例子

namespacedType = 'a/increment'
1

然后看registerMutation方法,给store._mutations数组中添加了一个wrappedMutationHandler方法,最终会执行传入的mutation 也就是说最后会被组和成

store = {
  _mutations: {
    'a/increment': [ƒ]
  }
}
1
2
3
4
5

Action 初始化

在循环阶段和mutation是类似的,不过因为action是支持异步的,所以在注册上有所不同

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  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
    )
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    return res
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

可以看到结果用Promsie.resolve进行包裹,且他可以获取的入参更多。最终获取的是

store = {
  _actions: {
    'a/increment': [ƒ]
  }
}
1
2
3
4
5

Getters初始化和响应式

循环也是一样就不说了,但是注册会有很大不同

function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    if (__DEV__) {
      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
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

有意思的是,我们在这里可以获取四个参数。并且我们在查看resetStoreVM的时候可以看到,它将_wrappedGetters转换成了computed放到了store._vm实例化的vuecomputed中实现响应式, 并且对store.getters进行了拦截,响应到了store._vm

使用

vuex 辅助函数

normalizeNamespace函数就是获取当前传入的参数中是否具有namespace字段,并且序列化成/xnormalizeMap 就是吧对象和数组两种形式都转换成对象的key value

normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
1
2

state的使用和mapState

在我们的使用过程中,最终其实是调用的computedcomputed会调用 watcher.evaluate()执行到this.getter.call(vm, vm),那也就是定义返回的mapState函数

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]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

因为是模块内,module.context其实就是之前定义的makeLocalContext(store, namespace, path),在这里面有个stategetters拦截器。这里会触发依赖收集,并且最终返回state[val]

getter 和 mapGetters 的使用

这个其实和state是一样的,state因为只是一个值,或者有可能是一个方法,所以它的返回需要包裹,但是getters必须是一个方法, 所以它只要正确的返回key对应的valcomputed去执行就可以了,所以它的mapGetters很简单 不赘述

mutations 、actions 和 mapMutations 、mapActions的使用

function mappedMutation (...args) {
  let commit = this.$store.commit
  if (namespace) {
    const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
    if (!module) {
      return
    }
    commit = module.context.commit
  }
  return typeof val === 'function'
    ? val.apply(this, [commit].concat(args))
    : commit.apply(this.$store, [val].concat(args))
}
1
2
3
4
5
6
7
8
9
10
11
12
13

和前面类似,如果是module就调用local中的 commit方法,如果不是就调用构造函数中申明的commit。在localcommit就是通过 namespace拿到对应的发行,最终还是调用commit函数

actionmutation是类似的,不过actions调用的是 dispatch函数,最后返回了一个Promise。然后交给commit执行。

Last Updated: