vuex.js源码学习 —— 初始化与实例方法

官方文档中对Vuex的描述如下:

Vuex 是一个专门为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件状态,并以相应的规则保证状态以一种可预测的方式发生变化。

Vuex 和 Redux 都是对Flux架构的实现,不同的是 Redux 是不需要感知视图层的,一个单纯的独立的状态管理工具,只要你愿意完全可以在 Vue 项目中使用 Redux 进行状态管理。

我们知道一个storestategettersmutationsactionsmodules几部分构成,它们之间的关系如下图所示:

今天这篇文章我们主要来看一下当接收到这些选项后,Vuex 是如何来处理它们的,最后看一下常用的实例方法commitdispatch的实现过程。

我没有在文中大量的贴源代码,一定要将代码 clone 到本地对着看哦。

准备工作

运行项目中的例子

1
2
3
git clone git@github.com:vuejs/vuex.git --depth 1
npm install
npm run dev

vuex对外的导出

Vuex 对外导出了不止Store一个类,打开index.js可以看到,它提供的功能如下:

1
2
3
4
5
6
7
8
9
10
11
export default {
Store, // 我们主要使用的类
install, // 提供给 Vue.use 用于安装Vuex的
version: '__VERSION__', // Vuex版本
mapState, // 为组件创建计算属性以返回 Vuex store 中的状态。
mapGetters, // 为组件创建计算属性以返回 getter 的返回值。
mapMutations, // 创建组件方法提交 mutation,会映射为 this.$store.commit('xxx')
mapActions, // 创建组件方法分发 action,会映射为 this.$store.dispatch('xxx)
createNamespacedHelpers, // 创建基于命名空间的组件绑定辅助函数。
createLogger // Vuex 自带一个日志插件用于一般的调试
}

后面我们会看到常用的辅助方法是如何实现的。

使用store

首先导入 Vuex 和 Vue:

1
2
3
4
import Vue from 'vue'
import Vuex from 'vuex' // 这里的 Vuex 是一个包含了上面那些内容的对象

Vue.use(Vuex)

定义 store 的构成部分,也就是用于创建 Store 实例的选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// define state tree
const state = {
// ...
}

// mutate state
const mutations = {
// ...
}

// commit mutations
const actions = {
// ...
}

// getters are functions
const getters = {
// ...
}

导出 Store 实例对象:

1
2
3
4
5
6
7
8
// A Vuex instance is created by combining the state, mutations, actions,
// and getters.
export default new Vuex.Store({ // 注意这里使用的 Vuex.Store,前面说了 Vuex 是一个对象
state,
getters,
actions,
mutations
})

将 Store 实例作为 Vue 实例的选项:

1
2
3
4
5
new Vue({
el: '#app',
store, // store 作为一个选项
// ...
})

install

安装store的过程主要在文件mixin.js中,我们来看一下 Vuex 是如何来扩展 Vue 的功能的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default function (Vue) {
// 我们只关心给 Vue2.x 的实现就好了
Vue.mixin({ beforeCreate: vuexInit })

/**
* Vuex init hook, injected into each instances init hooks list.
*/

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组件注入了beforeCreate选项,到时所有的Vue实例在初始化时都能调用到vuexInit函数了。

vuexInit函数中主要是为Vue实例添加$store属性。先判断了当前Vue实例的选项中是否有store,如果有的话这个Vue实例就是根实例了,那么直接使用该store。否则判断其父实例是否有$store属性了,如果有的话,使用父实例的$store。这样就解决了子组件实例也能正确访问到$store的问题。

接下来我们开始看Store类的实现。

初始化

先来看Store的构造函数,我们主要关心下面这几个地方的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export class Store {
constructor (options = {}) {
// ...

this._modules = new ModuleCollection(options)

// ...

const state = this._modules.root.state

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

// ...
}
// ...
}

Vuex 也允许我们将 store 分割成模块,以此避免在应用复杂时,store 对象会变得很臃肿。所以在 store 对象初始化时先使用传入的选项初始化了模块,然后安装模块,最后初始化store._vm。接下来详细看这几部分。

初始化模块

ModuleCollection的构造函数如下:

1
2
3
4
5
6
7
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
// ...
}

很简单,就直接调用了register方法,再来看一下这个方法做的事情。

在开发环境下会先判断下面这个几个选项的传的值是否正确:

1
2
3
4
5
const assertTypes = {
getters: functionAssert, // getters 中的key对应的值必须是函数
mutations: functionAssert, // mutations 中的key对应的值也必须是函数
actions: objectAssert // actions 中的key 对应的值可以是函数,也是一个带有handler的对象
}

actions是对象的情况,主要是在带命名空间的模块中想要注册全局action的时候使用的。

1
2
3
4
5
6
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}

接着register方法开始构造模块树,模块是可以嵌套的,一个嵌套模块的大概构成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
modules: {
moduleA: {
// 省略state等其他选项...
modules: {
moduleB: {
// 省略state等其他选项...
},
moduleC: {
// 省略state等其他选项...
modules: {
moduleD: {
// 省略state等其他选项...
}
}
}
}
}
}

先是创建module对象,一个module对象的构成如下:

1
2
3
4
5
6
{
runtime: false, // 标识是否是程序运行过程中动态注册的模块
state: {}, // 存储开发者传入的原始模块中的state
_children: {moduleA: Module}, // 存储子模块
_rawModule: {modules:{...}} // 该module对象对应的原始的modules
}

如果path是空数组,那此时创建的是根模块,直接将module对象赋值给this.root。如果不是的话,先找到这个模块的父模块,然后将其放入它的_children中。这个过程在处理上还蛮有技巧的,在递归调用register时传入的是path.concat(key),将当前要注册的模块放在了path的最后面。如果拿我们上面举的例子,那每次传给registerpath如下:

1
2
3
4
5
[]
['moduleA']
['moduleA', 'moduleB'],
['moduleA', 'moduleC'],
['moduleA', 'moduleC', 'moduleD']

所以你会发现一个元素的上一个元素就是它的父模块,而当前要找的是最后一个元素的父模块,所以给get方法传了最后一个元素之前的所有元素,然后在get中使用reduce方法最后拿到了传给它的最后一个元素的module对象。

经过这一步建立了模块之间的关系,生成了模块树。

安装模块

再接着看installModule方法,这里仍然是一个递归的过程,主要的处理流程如下:

  • 将模块注册到命名空间的映射表中。getNamespace方法返回的值是将从根模块到当前模块的模块名称用/分割构成的字符串。比如 moduleB 的命名空间是moduleA/moduleB/

  • 设置state能够嵌套调用,同时也设置为state为响应式的。这里要注意的是传给getNestedState方法的始终是rootState,也就是根模块的state。比如下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    export default new Vuex.Store({
    modules: {
    moduleA: {
    state: () => ({ stateA: 'a' }),
    modules: {
    moduleB: {
    state: () => ({ stateB: 'b' })
    },
    moduleC: {
    state: () => ({ stateC: 'c' }),
    modules: {
    moduleD: {
    state: () => ({ stateD: 'd' })
    }
    }
    }
    }
    }
    }
    })

    最终得到rootState,也就是store._modules.root.state的内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    {
    moduleA: {
    moduleB: {
    stateB: 'b',
    '__ob__': Observer
    },
    moduleC: {
    moduleD: {
    stateD: 'd',
    '__ob__': Observer
    },
    stateC: 'c',
    '__ob__': Observer
    },
    stateA: 'a',
    '__ob__': Observer
    }
    }

    这使得之后我们可以通过$store.moduleA.moduleB.stateB这种形式调用到每个模块中的state,并且因为是响应式的,当state改变时组件也会跟着刷新。

  • 生成为模块所用的局部context,其中包括dispatchcommitgetters以及state。我们看一下makeLocalContext方法是如何来处理的。

    对于dispatchcommit,如果没有使用命名空间的情况下就直接是store.dispatchstore.commit。如果使用了命名空间,则会对store.dispatchstore.commit进行一层包装,将传给它们的type的前面拼接上命名空间。举个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    export default new Vuex.Store({
    modules: {
    moduleA: {
    namespaced: true,
    state: () => ({
    count: 0
    }),
    mutations: {
    increment: state => {
    state.count++
    }
    },
    actions: {
    increment: ({ commit, state }) => {
    // 实际上调用的 store.commit('moduleA/increment')
    commit('increment')
    }
    }
    }
    }
    })

    这样就使得在不同的模块中定义了相同名字的actionsmutations不会互相影响,并且在模块中使用commitdispatch时也不用特意指定命名空间的路径。但是注意到下面这句判断:

    1
    if (!options || !options.root)

    这使得我们也可以调用到全局的actionsmutations,比如上面的例子中如果要提交的是全局的increment,可以这样写:

    1
    2
    3
    4
    5
    6
    actions: {
    increment: ({ commit, state }) => {
    // 这样调用到就是 store.commit('increment')
    commit('increment', null, { root, true })
    }
    }

    对于gettersstate使用Object.defineProperties进行了代理,在实际调用时才去获得它们的值,因为在运行期间Vue实例有可能修改它们。makeLocalGetters方法中创建了getters的代理对象gettersProxy,并对传入的type去掉了命名空间路径,这样在每一个getter在被调用时可以从store.getters[type]中取值。

  • 注册mutations & actions & getters,我们先举个例子,之后会看一下每一部分注册后的结果是什么。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const moduleA = {
    namespaced: true,
    state: () => ({
    count: 0
    }),
    getters: {
    evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
    },
    mutations: {
    increment: state => {
    state.count++
    }
    },
    actions: {
    increment: ({ commit, state }) => {
    commit('increment')
    }
    }
    }
    • 注册mutations,代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      module.forEachMutation((mutation, key) => {
      // 存储的是带有命名空间的key
      const namespacedType = namespace + key
      registerMutation(store, namespacedType, mutation, local)
      })

      function registerMutation (store, type, handler, local) {
      // 可以看到同一个类型的 mutation 可能会有多个 handler
      const entry = store._mutations[type] || (store._mutations[type] = [])
      entry.push(function wrappedMutationHandler (payload) {
      // ...
      }
      }

      注册之后,上面的例子中此时store._mutations的内容如下:

      1
      2
      3
      {
      'moduleA/increment': [f wrappedMutationHandler (payload)]
      }
    • 注册actions,代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      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) {
      // 可以看到同一个类型的 action 可能会有多个 handler
      const entry = store._actions[type] || (store._actions[type] = [])
      entry.push(function wrappedActionHandler (payload) {
      // ...
      }
      }

      注册之后,上面的例子中此时store._actions的内容如下:

      1
      2
      3
      {
      'moduleA/increment': [f wrappedActionHandler (payload)]
      }
    • 注册getters,主要的代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      module.forEachGetter((getter, key) => {
      const namespacedType = namespace + key
      registerGetter(store, namespacedType, getter, local)
      })

      function registerGetter (store, type, rawGetter, local) {
      // ...
      store._wrappedGetters[type] = function wrappedGetter (store) {
      // ...
      }
      }

      注册之后,上面的例子中此时store._wrappedGetters的内容如下:

      1
      2
      3
      {
      'moduleA/evenOrOdd': ƒ wrappedGetter(store)
      }

    后面我们看到对缓存起来的mutationsactionsgetters的使用。

初始化store._vm

Store类自己创建了一个Vue实例_vm,来建立起stategetters的依赖关系,我们知道getters的返回值一般都是要依赖于state的。接下来我们看一下resetStoreVM方法是如何做到的,先看下面这段:

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

当我们访问store.state时,实际上访问的是下面这个get方法:

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

这样就前后联系起来了。然后再看一下computed的生成过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
store.getters = {}
// 清空之前对getters的缓存,之后每次访问getters时会重新加入
store._makeLocalGettersCache = Object.create(null)
// store._wrappedGetters 中存储的内容是在上一步注册getters时完成的
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})

上面已经分析出store._wrappedGetters存储的内容了,对其进行遍历时,fn就是wrappedGetter (store)keymoduleA/evenOrOdd。所以computed中最后存储的内容为:

1
2
3
4
5
{
'moduleA/evenOrOdd': function () {
return wrappedGetter(store)
}
}

同时在store.getters中也添加了moduleA/evenOrOdd,并将它的值代理到store._vm['moduleA/evenOrOdd']。因为Vue实例代理了计算属性,所以可以直接从_vm中访问该属性。

当访问store.getters['moduleA/evenOrOdd']时,实际访问的是store._vm['moduleA/evenOrOdd'],触发了计算属性的执行,这样就会去执行wrappedGetter (store)。我们前面看到wrappedGetter (store)中会执行rawGetter(local.state, ...),就会访问到store.state,也就是store._vm._data.$$state,这样就将stategetters建立起了联系,并且每次访问getters时使用的都是最新的state中的值。

Vuex.Store只有两个实例属性,stategetters,现在我们已经知道它们是如何实现的了。

主要的实例方法实现

subscribe

subscribe方法主要提供给开发者在每个mutation执行结束后,处理一些事情。它的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
subscribe (fn, options) {
return genericSubscribe(fn, this._subscribers, options)
}

function genericSubscribe (fn, subs, options) {
if (subs.indexOf(fn) < 0) {
options && options.prepend
? subs.unshift(fn)
: subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}

可以看到就是使用subscribe方法向内部的_subscribers数组添加了一个handler。它还是接收一个options参数,如果在其中指定了prependtrue,那handler就会添加到_subscribers的第1位,默认情况下会直接追加到数组中。

而且可以看到subscribe方法的返回值也是一个函数,在这个函数中会将该handler_subscribers中移除掉。

官方文档中介绍handler会接收两个参数mutationstate,在接下来的commit方法中会看到这个传参。

commit

commit方法是用来提交mutations的,它内部的处理流程如下:

  • 处理传参。因为commit即支持使用type字符串的传参方式,也支持使用对象风格的传参方式,所以先将传参规范化,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    commit (_type, _payload, _options) {
    // check object-style commit
    const {
    type,
    payload,
    options
    } = unifyObjectStyle(_type, _payload, _options)

    // ...
    }

    使用type字符串的传参时写法如下:

    1
    store.commit('increment', 10)

    使用对象风格的参数时写法如下:

    1
    2
    3
    4
    store.commit({
    type: 'increment',
    amount: 10
    })

    可以看到commit还接收一个参数options,其中可以指定root,如果为true,会提交全局的mutations。这个在上面创建module.context时已经看到了对root的使用。

  • 提交mutations。将这一段代码简化后如下:

    1
    2
    3
    4
    5
    const mutation = { type, payload }
    const entry = this._mutations[type]
    entry.forEach(function commitIterator (handler) {
    handler(payload)
    })

    现在取出了_mutations中存的handler,前面我们已经分析出这里的handlerwrappedMutationHandler (payload)函数,它的内容如下:

    1
    2
    3
    function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
    }

    从这里可以看出,mutations在执行时的上下文是store,它接收两个参数:本模块的statepayload

  • 执行_subscribers数组中通过subscribe方法添加进来的handler

    1
    2
    3
    this._subscribers
    .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
    .forEach(sub => sub(mutation, this.state))

    这里看到给handler传了当前的mutationstate两个参数。

subscribeAction

subscribeAction主要用来订阅storeactions。它的实现代码如下:

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

genericSubscribe方法在上面分析subscribe方法时已经分析过了,这里是向_actionSubscribers数组中添加的处理器。

subscribeAction接收到参数后,先对其进行了转换,如果传的函数,让它作为before时的处理函数。这是因为subscribeAction支持指定处理函数的被调用时机是在一个action分发之前还是之后,默认是之前。

1
2
3
4
5
6
7
8
store.subscribeAction({
before: (action, state) => {
console.log(`before action ${action.type}`)
},
after: (action, state) => {
console.log(`after action ${action.type}`)
}
})

dispatch

dispatch方法主要是用来分发actions的,它内部的处理流程如下:

  • 处理参数。跟commit一样,dispatch也支持两种风格的传参方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 以载荷形式分发
    store.dispatch('incrementAsync', {
    amount: 10
    })

    // 以对象形式分发
    store.dispatch({
    type: 'incrementAsync',
    amount: 10
    })
  • 执行通过subscribeAction方法添加的before处理函数,代码如下:

    1
    2
    3
    4
    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))
  • 分发actions,对应的代码如下:

    1
    2
    3
    const result = entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)

    我们上面看到,对于actions来说,一个type可以对应多个处理器,所以这里这里使用了Promise.all来执行所有的处理函数。并且上面已经分析出handler就是wrappedActionHandler函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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)
    }
    if (store._devtoolHook) {
    return res.catch(err => {
    store._devtoolHook.emit('vuex:error', err)
    throw err
    })
    } else {
    return res
    }
    }

    可以看到在执行actions的时候,其内部的this会指向store,并且它能接收到的参数如下:

    1
    2
    3
    4
    5
    actions: {
    increment: ({ dispatch, commit, state, getters, state, rootGetters, rootState }) => {
    commit('increment')
    }
    }

    并且一个action处理函数会始终返回一个Promise对象。同样地,dispatch执行后也会返回一个Promise对象,代码如下:

    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
    return new Promise((resolve, reject) => {
    result.then(res => {
    try {
    // 先执行了 after 处理函数
    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)
    }
    }
    // 然后将前面得到的 result 直接 resolve出去
    resolve(res)
    }, error => {
    try {
    // 当出错时执行 error 处理函数
    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)
    })
    })

    因为actions中大部分情况是异步操作,用Promise包装后可以很容易知道异步执行什么时候结束,并且可以组合多个actions

Vuex 的实现就先分析到这,基本上核心内容都已经看过了,其他的方法不是很难,有兴趣可以自己找时间看一下。

本文作者:意林
本文链接:http://shinancao.cn/2020/03/20/Vue-Source-Code-20/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!