Vuex使用指南及原理简介

一、Vuex用法指南

1. Vue和Vuex的关系

Vuex是专为vue设计的状态管理框架,类似(并且借鉴自)React的Flux。

我们知道,Vue的设计借鉴了MVVM(Model-View-ViewModel)的思想,也就是由业务数据驱动视图渲染。它使用template来定义视图模板,用data来管理业务数据。通过将data中的数据绑定到模板,并对data的值进行监听,来自动渲染和更新视图。比如下面的组件:
ArticleTitle.vue

<template>
    <h1>{{title}}</h1>
</template>

<script>
    data () {
        return {
            title: 'Vuex指南'
        }
    }
</script>

这个组件所使用的模板由template定义,模板内通过{{ title }}插值语法,将业务数据data中的变量title绑定进来。首绘时页面会渲染title的初始值:'Vuex指南',以后任何时候只要修改title的值,视图都会依据新的值重新渲染。

业务数据可以跨组件传递,但是只能从上级组件向下级组件传递,不能逆向或跨兄弟组件(泛指没有嵌套关系的组件)传递。如果子组件需要修改父组件中的数据,需要向父组件触发update事件来更新,不允许直接修改。

那么问题就来了:假如有一组业务数据,它在多个组件的视图中会用到,并且这些组件没有直接的嵌套关系,那么该在哪保存和管理这个数据,才能保持所有组件的引用是同步的呢?

Vue给出的官方解决方案是,使用Vuex将这组数据作为公共数据提取出来,单独维护。所有组件都直接与这组公共数据交互,从而保证组件状态的同步。

为了对这组公共数据进行规范管理,Vuex定义了五个重要概念:state、getters、mutations、actions和modules。

  1. state的含义是“状态”。在计算机学科中,用于描述某个实体运行规则的数学模型被称为“状态机”,状态机的某个特定的状态被称为state。在这里,对共享数据的管理逻辑可以抽象为一个状态机,state描述的就是它管理的这组共享变量,从state中你可以知道被管理的变量在当前状态下的值。
  2. getters是Vuex版本的计算属性。
  3. mutations是Vuex提供的修改state中变量值的唯一方法。
  4. actions是封装数据处理逻辑的一般方法,常用来封装异步操作。
  5. modules是Vuex的模块拆分方案,用于解决由变量过多导致的难以维护的问题。

Vuex最简单的用法如下:
main.js

import Vue from 'vue';
import Vuex from 'vuex';
import App from './App.vue';

Vue.use(Vuex);

const store = Vuex.store({
  state: { name: 'carter' },
  mutations: { 
    setName: (state, name) => {
      state.name = name;
    }
  }
})

new Vue({
    store,
    render: h => h(App)
}).$mount('#app');

现在在整个vue应用的任何一个组件内,我们都可以通过this.$store访问到这个全局store,包括从state中取值,或通过mutations提交修改等(这里的逻辑比较简单,因此我们没有定义getters、actions和modules等属性)。如:
App.vue

<template>
  <h1>{{name}}</h1>
</template>

<script>
  data(){return {};},
  computed: {
    name: this.$store.state.name;
  }
</script>

通过当前组件对象this上的$store属性,即可访问到全局store,它提供了state、getters、mutations、actions等属性。

下面我们分别详细介绍这五个概念。

2. 核心概念介绍

(1). state

如果与Vue的概念进行类比,Vuex中的state可以理解为Vue的data,即业务数据。但是Vuex的定位是状态管理,它不负责任何View(视图层)和ViewModel(绑定层)的逻辑,仅仅用于维护共享变量的状态,因此它沿用了状态机中的概念,用state来访问变量。

由于Vuex是依赖Vue的状态管理框架,因此它的状态也是响应式的。等价的,它与Vue有着同样的约束,如不能直接通过索引修改数组某项的值,为对象新增的属性不是响应式的等。相应的解决方案与Vue也是一致的,这里不再赘述。

关于state,最重要的是要记住它维护的变量值永远只能通过mutations来修改,这是为了保证数据变化的可追踪(关于Vuex如何追踪数据变化,后面的原理部分会结合部分源码介绍),提高数据的可维护性。

在组件中引用state,可以通过this.$store.state,一般我们会将其映射到一个计算属性,如:

...
computed: {
  name() {
    return this.$store.state.name;
  }
}
...

也可以不映射到计算属性,而是在组件的任意地方直接访问这个值(在模板中使用时不需要this),如:

<template>
  <h1>{{ $store.state.name }}</h1>
</template>
...
methods: {
  getName(){
    return this.$store.state.name;
  }
}

如果需要将state的多个值分别映射到组件的计算属性,一个个像上面那样写未免过于麻烦。为此Vuex提供了一个辅助函数mapState,可以帮助你少写一些代码:

import { mapState } from 'vuex';
export default {
  // ...
  computed: mapState({
    count: state => state.count,
    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

注意,只有使用常规函数,才能访问当前组件对象this,箭头函数内部是没有定义this的。如果不需要为变量改名,mapState也可以接受一个字符串数组:

...
computed: mapState(['count', 'name']),
...

这样你就把state中的变量countname映射成了当前组件的同名计算属性countname。于是你可以在模板中这样方便地绑定它的值:

<template>
  <h1>{{ count }}</h1>
</template>

使用es6的展开符可以组合state中的变量和常规计算属性:

computed: {
  ...mapState(['count', 'name']),
  menuLength() {
    return this.menus.length;
  }
}

由于组件中不允许直接修改state中变量的值,只能用于取值,所以它和计算属性一样,是“只读”(狭义上的只读,指不允许直接进行写操作)的。又因为state和计算属性一样都是响应式的,因此我们才常把state中的变量映射到组件的计算属性。

不过state中的变量并不是真正意义上的“只读”,它可以通过提交mutation来修改,后面介绍mutation时会介绍。

(2). getters

getters之于Vuex,就如computed之于Vue,用于定义较为复杂的取值逻辑。

现在假设我们在state中有一个菜单数组menus,它的每一项带一个root属性,标识当前菜单是否为根节点:

state: {
  menus: [
    { id: '1', ..., root: true }
    { id: '2', ..., root: true }
    { id: '11', ..., root: false }
    ...
  ]
}

假如我们需要在很多个组件中用到由根节点组成的菜单数组,常规的思路是在组件中定义一个计算属性,或者定义一个函数来过滤根节点,如:

computed: {
  rootMenu() {
    return this.$store.state.menus.filter((menu) => {
      return menu.root;
    })
  }
}
// 或
methods: {
  getRootMenu(){
    return this.$store.state.menus.filter((menu) => {
      return menu.root;
    })
  }
}

由于计算属性可以基于依赖对结果进行缓存,因此第一种实现的性能高于定义函数。但如果有许多组件都依赖这个值,并且实际的取值复杂度原高于这里呢?你需要在每个组件都重写这个复杂的取值逻辑!

或者你为了方便,直接把根菜单数组保存在store里(假设是rootMenu)。但这是不可取的,因为假如menus发生变化,rootMenu的值就可能受影响,所以你必须在维护menus的值时,同时去维护rootMenu,这就显著增加了mutations逻辑的复杂性,以及系统出bug的概率。

Vue中为了解决这个问题,提出了computed,对计算结果进行缓存,Vuex则提出了getters。

我们可以用getters这样定义rootMenu:

state: { menus: [...] },
getters: {
  rootMenu(state) {
    return state.menus.filter((menu) => {
      return menu.root;
    })
  }
}

在组件中使用getters很简单:

computed: {
  rootMenu() {
    return this.$store.getters.rootMenu;
  }
}

或者像state一样,用辅助函数mapGetters快速映射到组件的计算属性:

import { mapGetters } from 'vuex';
// ...
computed: {
  ...mapGetters(['rootMenu'])
}

现在在组件中,我们直接使用getters中的rootMenu即可,不需要重新封装处理逻辑。最重要的是,它和computed一样,可以对计算结果缓存,并且会在所依赖的变量变化时自动更新。

有人可能会好奇,为什么getters中的命名使用的是名词,而不是诸如getRootMenu这样的命名法呢?

其实这和Vue的computed是一个道理。在Vuex中,getters是作为state变量的计算属性存在的,也就是说,getters的每一项被Vuex视为一个变量(从表面上看它虽然是函数,但那只是为了封装计算过程,它在逻辑上被视为变量),只是它的值依赖于state中其他变量的值。

getters可以接受store的getters属性作为第二个参数,通过它可以访问其他getters,这样可以组合出更加复杂的取值逻辑:

getters: {
  rootMenuLength(state, getters){
    return getters.rootMenu.length;
  },
  rootMenu(state) {
    return state.menus.filter((menu) => {
      return menu.root;
    })
  }
}

另外,getters也是可以接受参数的,不过此时的getters必须返回一个函数。比如我们需要传入菜单的level过滤出某个层级的菜单节点:

getters: {
  getMenuByLevel(state) {
    return function(level) {
      return state.filter(menu => {
        return menu.level === level;
      })
    }
  }
  // 或者这样写
  getMenuByLevel: (state) => (level) => {
    return state.filter(menu => {
      return menu.level === level;
    })
  }
}

可以看到,这时getters不再用名词命名,因为从功能逻辑上它已经变成一个根据level查询结果的函数,此时它应该这样调用:

...
let secondMenuList = this.$store.getters.getMenuByLevel(2);
...

并不是从state中取值总要定义getters,getters仅用于封装复杂的取值逻辑。

(3). mutations

用于向store提交一个修改事件,这是Vuex提供的修改state变量值的唯一方法。

注意,并不是说下面的语法无法修改state的值:

this.$store.state.name = '张三';

这个语句从正确性来说没有任何问题,它会正确修改state.name的值,并且会正常触发响应式系统。

它的唯一问题是,当有多个组件依赖这个变量的值时,没人知道是哪个组件修改了它的值,也不知道是何时修改的,这个变量的修改过程无法被追踪。假如当前系统非常庞大,数据依赖复杂,这种无法追踪的修改会导致代码难以调试和维护。

为此,Vuex规定,state中变量的值必须通过事件来修改,这样Vuex就可以通过注册钩子函数来追踪状态变化,这也是各大状态管理框架的常规思路。

你可以像下面这样定义一个更新state的逻辑:

state: { name: 'carter' },
mutations: {
  setName(state, name) {
    state.name = name;
  }
}

这里定义了一个修改事件:setName,Vuex会在调用它的时候自动传入当前state对象,通过它你可以完成对state的修改。在组件中,如果你想修改state中的值,可以通过提交这个事件,并传入参数:

this.$store.commit('setName', '张三');
// 或者
this.$store.commit({type: 'setName', name: '张三'});

Vuex在Store的原型上提供了一个可以订阅修改事件的方法:subscribe,如果你需要编写一个在state的值发生修改时进行处理的Vuex插件,一定会用到这个方法(Vuex自身提供的状态追踪,本质上就是通过这个方法注册监听mutation事件的钩子函数实现的)。

通过mapMutations辅助函数,你可以把store中的mutations映射成组件的方法:

import { mapMutations } from 'vuex';
...
methods: {
  ...mapMutations(['setName']),
  localFn (name) {
    this.setName(name);
  }
}

mapMutations的操作实际上非常简单,它只是将setName处理成以下实现:

methods: {
  setName(name) {
    this.$store.commit('setName', name);
  }
}

mutations有一个严格的规定,就是禁止包含异步操作,这是为了让Vuex能正确追踪state的变化。

Vuex追踪state变化的逻辑是,向所有的mutations事件注册一个钩子函数,一旦某个mutation函数(如setName)执行完毕,Vuex会立即查询当前state的状态,并显示在devtools中。注意,查询state状态的回调函数是提前注册好的,它不会等待mutation中的异步逻辑执行完才调用,也就是说,Vuex在state还未发生变化时就去查询了当前state的状态,而state真正变化时,Vuex并不知道,这样Vuex就无法实现对state的追踪了。

注意,在mutations中包含异步逻辑并不会导致系统异常,它只是会让Vuex的追踪功能(以及其他与state相关的功能)失效。

那么遇到需要异步处理的逻辑该怎么办呢?答案就是提交actions

(4). actions

actions是为了解决mutations不能包含异步代码而设计的。我们可以这样理解actions:它封装了一些异步操作,这些异步操作的结果将用于修改state的值,当它得到这些值时,会通过提交mutations来修改state的值。

言外之意,actions不修改state的值,它对state造成的影响是通过提交mutations体现的。下面是一个简单的actions:

import axios from 'axios';
...
state: { name: 'carter' },
mutations: {
  setName(state, name) {
    state.name = name;
  } 
},
actions: {
  fetchName(store, url) {
    axions.get(url).then(res => {
      store.commit('setName', res.data);
    })
  }
}

在组件中这样分发一个action:

...
this.$store.dispatch('fetchName', url);
...

这个语句会触发名为fetchNameaction,并传入参数url。Vuex会调用对应的函数,传入当前的store对象,并将url作为第二个参数传入。然后在函数内,axios会发送一个请求,得到用户名后通过store.commit('setName', res.data)提交修改name的事件,state.name随即发生变化。

如果你不需要完整的store对象,可以用es6的解构赋值提取需要的方法:

fetchName({ commit }, url) {
    axions.get(url).then(res => {
      commit('setName', res.data);
    })
  }

store的所有属性都可以这样提取出来,以方便使用。

actions可以像mutations一样映射为组件方法,这需要使用mapActions辅助函数:

import { mapActions } from 'vuex';
...
methods: {
  ...mapActions(['fetchName']);
}
// 调用格式为:this.fetchName(url);

在实际使用actions时,我们经常需要知道actions何时执行完毕,以为其定义后续的处理逻辑,或者在某些情况下组合使用多个action,这可以借助Promise或者async函数实现:

actions: {
  fetchName(store, url) {
    return new Promise(function(resolve, reject) {
      axions.get(url).then(res => {
        store.commit('setName', res.data);
        resolve();
      }).catch(e => {
        reject(e);
      })
    })
  }
  // async的版本
  async fetchName(store, url) {
    store.commit('setName', await axios.get(url));
  }
}

现在这个actions函数返回的是一个Promise对象,因此你可以为它注册后续的回调事件:

this.$store.dispatch('fetchName', url).then(res => {
  // state.name变化后的操作
})

你还可以组合使用action,来完成更复杂的逻辑,比如我们现在要提交一个actionB,但它需要另一个actionA执行完后才能执行,于是可以定义下面的action

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

现在当你在组件中分发一个actionB时,它在函数内会自动分发actionA,等待其执行成功,才会继续执行后面的逻辑。

Vuex在Store的原型上定义了subscribeAction方法,来订阅特定的action事件,并为其定义钩子函数。如果你需要编写处理action的Vuex插件,很可能需要这个方法。

(5). modules

这是为了对庞大的store进行模块化分解而设计的。

假如某个应用极其复杂,有上百个共享变量需要在state维护,与每个共享变量相关的getters、mutations、actions更是数不胜数,那么这个store的庞大程度可想而知。另外,假如store中的state由不同的开发人员维护,还可能发生命名冲突、意外修改等严重的问题。

为此,Vuex提出将state的变量按照一定逻辑划分成不同的模块,单独维护。每个划分出来的模块,就称为一个module。它包含自己专有的state、getters、mutations、actions,甚至可以继续定义自己的子模块modules:
moduleA

export const moduleA = {
  namespaced: true,  // 使用单独的命名空间
  state: {},
  mutations: {},
  actions: {}
}

index.js

import moduleA from './module/moduleA';

new Vuex.Store({
  state: {},
  mutations: {},
  actions: {}.
  modules: {
    moduleA
  }
})

module中有一个特殊的属性,叫namespaced,表示是否要使用单独的命名空间。默认情况下,这个值为false,即不启用单独的命名空间。

此时所有模块内部的getters、mutations和actions都是注册到全局命名空间的,也就是说向全局store提交的commit,或者派发的action也会被子模块监听到。如:

new Vuex.Store({
  state: {},
  mutations: { setName: (state) => {...} },
  modules: {
    moduleA: {
      state: {},
      muttations: { setName: (state) => {...} },
    }
  }
})

我们看到,全局空间和moduleA都定义了名为setNamemutation,并且moduleA没有启用单独的命名空间,因此下面的commit会导致两个都被触发:

this.$store.commit('setName', name);

而如果为moduleA启用单独的命名空间,moduleA就被高度封装了,上面向全局空间提交的commit不会再对moduleA生效。此时再想向moduleA提交一个commit必须这样:

this.$store.moduleA.commit('setName', name);

是否启用单独的命名空间要依具体的需求而定。

如果需要用辅助函数将模块内的state、getters、mutations和actions映射到组件内,需要用到Vuex提供的createNamespacedHelpers方法:

import { createNamespacedHelpers  } from 'vuex';
const { mapState, mapGetters, mapMutations, mapActions} = 
    createNamespacedHelpers('moduleA');
...
computed: {
  ...mapState([...]),
  ...mapGetters([...])
},
methods: {
  ...mapMutations([...]),
  ...mapActions([...])
}

上面的辅助函数不一定要全部导入,比如当前组件只需要moduleA的name和对应的mutation:setName,就可以这样写:

import { createNamespacedHelpers  } from 'vuex';
const { mapState, mapMutations} = 
    createNamespacedHelpers('moduleA');
...
computed: {
  ...mapState(['name']),
  // 其他计算属性
},
methods: {
  ...mapMutations(['setName']),
  // 其他method
}

在模块内部,getters、mutations和actions接受的第一个参数与全局作用域不同。

对于getters,它的第一个参数是当前模块的state,而不是根state。如果你需要在getters内访问根state或根getters,可以作为第三、四个参数传入:

getters: {
  name(state, getters, rootState, rootGetters){
    ...
  }
}

对于mutations,它只能访问当前模块的state。这就意味着你不能用当前模块内的mutation去修改根状态。

对于actions,它接收的第一个参数不再是全局的store对象,而是当前模块对应的上下文对象context。你可以通过它的rootState属性来访问根状态,以及它的rootGetters访问根getters

const moduleA = {
  // ...
  actions: {
    actionA ({ state, commit, rootState, rootGetters }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

在模块内可以注册全局的action:

modules: {
    moduleA: {
      namespaced: true,
      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... }
        }
      }
    }
  }

它会在向全局store派发action时被调用,但是却可以拿到当前模块的上下文对象(当然该对象的rootState,rootGetters也可以访问到全局state和getters)。

store在创建完成后仍然可以动态注册模块:

import Vuex from 'vuex'
const store = new Vuex.Store({ /* 选项 */ })

// 注册模块 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

动态注册的模块可以用store.unregisterModule('myModule')的语法卸载,但是该语法不能卸载静态模块(即初始化时创建的模块)。

3. 项目结构

Vuex的store是全局唯一的,因此一般直接在src下维护。

如果没有使用模块,而全局状态比较复杂,可以把state、getters、mutations和actions单独提出来,在index中整合,如:

src
  |- store
      |- index.js
      |- state.js
      |- getters.js
      |- mutations
      |- actions

index.js一般这样写:

import Vue from 'vue'
import Vuex from 'vuex'

import { state } from './state'
import { getters} from './getters'
import { mutations} from './mutations'
import { actions} from './actions'

Vue.use(Vuex);
export default Vuex.Store({
  state,
  getters,
  mutations,
  actions
}) 

main.js中这样引入store:

import App from './App.vue'
import store from './store'

new Vue({
  store,
  render: h => h(App)
}).mount('#app')

当然,如果store中维护的变量很少,可以直接在index.js中定义这些属性。

如果项目的store很复杂,需要用模块维护,一般会用一个文件夹对模块单独维护:

src
  |- store
      |- modules
          |- moduleA.js
          |- moduleB.js
      |- index.js

index.js需要这样改写:

import Vue from 'vue'
import Vuex from 'vuex'

import { moduleA } from './modules/moduleA'
import { moduleB } from './modules/moduleB'

Vue.use(Vuex);
export default Vuex.Store({
  state: {},
  ...
  modules: {
    moduleA,
    moduleB
  }
}) 

注意,即使使用模块,你也可以定义全局state、getters等,如果它足够复杂,也可以提取到外部文件,这完全视情况而定。

二、Vuex架构简介

1. Vue.use(Vuex)做了什么

这一部分我们会结合少量的Vuex源码,介绍Vuex工作的大致原理。

首先我们来看Vue.use(Vuex)做了什么。

我们知道,Vue.use是注册第三方组件的一般方法,它会去调用组件提供的install方法将组件注册到Vue上,下面看Vuex提供的install方法:

function install (_Vue) {
    if (Vue && _Vue === Vue) {
      {
        console.error(
          '[vuex] already installed. Vue.use(Vuex) should be called only once.'
        );
      }
      return
    }
    Vue = _Vue;
    applyMixin(Vue);
  }

前面的例行检查可以忽略,Vuex真正的注册逻辑就是调用applyMixin方法,我们看它的实现:

function applyMixin (Vue) {
    var version = Number(Vue.version.split('.')[0]);

    if (version >= 2) {
      Vue.mixin({ beforeCreate: vuexInit });
    } else {
      ... // 兼容Vue 2.0以前的代码,可以忽略
    }
    
    /**
     * Vuex init hook, injected into each instances init hooks list.
     */
    function vuexInit () {
      var 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以上的版本(2.0以下的实现是等价的,我们可以跳过),Vuex通过Vue.mixin({ beforeCreate: vuexInit })向每个组件的beforeCreate生命周期钩子中混入了一个方法:vueInit。也就是说,Vue在初始化任何一个组件,并且经历完beforeCreate这个生命周期后,都会执行这个vueInit函数。

接着来看vueInit的实现。它首先检查当前实例的$options是否包含store属性,这里的store属性就是我们在下面的初始化语句传入的。

new Vue({
  store,
  ...
})

如果存在store属性,Vuex会将其挂载到当前组件对象的$store属性上,供组件访问。

也就是说,如果在new Vue时传入了store对象,那么在创建每个组件,并且初始化到beforeCreate这个生命周期时,Vuex向Vue混入的vueInit钩子函数就会被调用,它会检查options是否传入了store对象,如果是,就添加到this.$store属性上(如果store是函数,会先执行它)。

这样,我们的store对象就成了组件对象的$store属性。

下面我们来看const store = new Vuex.Store({ ... })做了什么。

2. new Vuex.Store({ … })做了什么

Store是Vuex提供的构造函数,用于构造Store实例,它的实现如下:

var Store = function Store (options) {
    ... // 例行检查,如Vue和Vuex是否安装,是否通过new调用等
    
    // store internal state
    this._committing = false;
    this._actions = Object.create(null);
    this._actionSubscribers = [];
    this._mutations = Object.create(null);
    this._wrappedGetters = Object.create(null);
    this._modules = new ModuleCollection(options);
    this._modulesNamespaceMap = Object.create(null);
    this._subscribers = [];
    this._watcherVM = new Vue();
    this._makeLocalGettersCache = Object.create(null);

    // bind commit and dispatch to self
    var store = this;
    var ref = this;
    var dispatch = ref.dispatch;
    var commit = ref.commit;
    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)
    };

    // strict mode
    this.strict = strict;

    var 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);

    // apply plugins
    plugins.forEach(function (plugin) { return plugin(this$1); });

    var useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools;
    if (useDevtools) {
      devtoolPlugin(this);
    }
  };

跳过最前面的例行检查。首先,Vuex将传入的参数接收进来,保存在对应的属性内,同时定义一些用于管理订阅者、模块等的内部属性。

然后定义了触发mutation和action的commit和dispatch实例方法。

接着是初始化模块(installModule),每个模块基本上也可以看做一个store对象。

再接着是构建Vuex的响应系统(resetStoreVM)。到目前为止,Vuex并没有接入Vue的响应式系统,因此它的state变化并不会带来响应式效果,Vuex没有重新实现响应式系统,而是像下面一样用state和getters创建一个新的Vue实例:

function resetStoreVM (store, state, hot) {
    ...
    store._vm = new Vue({
      data: {
        $$state: state
      },
      computed: computed
    });
    ...
}

plugins.forEach语句是在安装Vuex插件,Vuex的插件一般是注册commit和dispatch事件的回调函数。

最后,devtoolPlugin是将Vuex注册到Vue的调试工具devtools。

除了初始化之外,Vuex还定义了一些有用的原型函数,如Store.prototype.subscribe,它可以向commit事件注册回调函数,在插件开发时非常有用,我们这里不再详细介绍,想要了解的可以去阅读Vuex源码。

总结

Vuex的使用不算特别复杂,源码逻辑也较为清晰(ps:本人只阅读了大致结构),而且在Vue开发中占有很重要的地位,因此作为Vue的开发者,认真学习它还是很有必要的。

猜你喜欢

转载自blog.csdn.net/qq_41694291/article/details/106313710
今日推荐