Vuex

1,165 阅读5分钟

用法说明

Vuex把组件的共享状态抽出来,以一个全局单例模式管理。每一个Vuex应用的核心就是store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分 状态(state)。Vuex和单纯的全局对象有以下两点不同:

  1. Vuex的状态存储是响应式的。当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应的得到高效更新。
  2. 你不能直接修改store中的状态。改变store中的状态的唯一途径就是显示地提交(commit)mutation。这样使得我们可以方便地跟踪每一个状态的变化。 解读1:Vuex的存储的数据state是响应式的,一处组件修改 state 的数据,其他用到的地方会自动变化。原理是Vuex采用了new一个Vue对象来实现数据的“响应式化”,运用Vue.js内部提供的数据双向绑定功能来实现store的数据与视图的同步更新。

解读2:修改数据通过mutation的方式,而非直接修改 store.state.count ,是因为我们想更明确地追踪到状态地变化。修改数据的入口统一起来。还有一点,如果我们使用了vue 的 devtools工具的话,直接修改 store.state.count = 100;是无法在devtools体现出来。同样地,我们在mutation中处理地是异步代码修改state的值,devtools也无法追踪到。

state

state:用来存放组件共享的数据,对数据进行初始化。 通过在根实例中注册store选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。

const app = new Vue({
  el: '#app',
  // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})

我们可以通过 this.$store.state 访问到定义的全局状态。一般会在子组件中通过 computed 接收。

getter

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

mutation

更改Vuex的store中的状态的 唯一 方法是提交 mutation。同时,mutation里面一般是进行同步修改state中的数据,如果我们进行了异步修改,只是devtools里面追踪不到。同时这样也不规范。

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

我们通过 store.commit('increment') 的方式调用mutations里面的方法修改state的数据。原理是发布订阅模式。

action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

我们通过 store.dispatch('increment') 来触发 actions。

mudule

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

问题

看了上面的用法说明,善于发现问题的你应该多些反问。

  • 为什么 mutation 是修改 state 的唯一方式呢?直接修改 store.state.count = 100; 会怎么样呢?
    页面数据 count 会变为 100, 但是 devtools 里面并没有修改还是原来的值,出现了数据不一致,无法追踪数据变化。

  • actions 里为什么也要通过 mutation 修改数据呢,直接修改数据 context.state.a = "actions"; 会怎么样呢?
    页面数据 a 会变为 actions, 但是 devtools 里面并没有修改还是原来的值,出现了数据不一致,无法追踪数据变化。

  • mutation 进行异步 setTimeout 修改会怎么样呢?
    页面数据会修改,但是 devtools 里面的值还是原来的并没有修改。出现了数据不一致,无法追踪数据变化。

Vuex有一种严格模式,在 new Vuex.Store({ strick: true, ...}) 的时候,开发模式下,只能通过 mutation修改state了,通过 store.state.count这种方式会报错。同时 devtools 追踪不到。只有mutation修改的state才会被devtools追踪记录下来。 同时,在mutation异步修改state的值,页面响应变化了,但是devtools的数据并没有更新。10000 和 9999. 上图:

可以看到devtools只能追踪记录通过mutation同步修改的数据

Vuex 源码分析

我们知道在使用Vuex的时候需要通过四步来完成:

  1. import Vuex from 'vuex'
  2. Vue.use(Vuex)
  3. 初始化 store
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})
  1. 将store实例传给vue对象
var app = new Vue({
	el: '#app',
	router,
  store,
	components: {},
	data: {}
});

我们来分析关键的第二步和第三步。

Vue.use(Vuex)

我们知道 Vue.use(Vuex) 的时候会调用 Vuex定义的install方法。进入 node_modules/vuex/dist/vuex.esm.js文件中,找到 install 方法。顺藤摸瓜的调用顺序是这样的:install (_Vue) -> applyMixin(Vue) -> vuexInit 在 vuexInit 方法中,将 $store 放到组件的 this.$store中。这样在所有组件通过 this.$store 访问到定义的全局数据。

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

初始化 store

找到Store定义的 class 的构造函数,其中又有关键的三个函数:this._modules = new ModuleCollection(options);
installModule(this, state, [], this._modules.root);
resetStoreVM(this, state);

var Store = function Store (options) {
  var this$1 = this;
  if ( options === void 0 ) options = {};

  // Auto install if it is not done yet and `window` has `Vue`.
  // To allow users to avoid auto-installation in some cases,
  // this code should be placed here. See #731
  if (!Vue && typeof window !== 'undefined' && window.Vue) {
    install(window.Vue);
  }

  if ((process.env.NODE_ENV !== 'production')) {
    assert(Vue, "must call Vue.use(Vuex) before creating a store instance.");
    assert(typeof Promise !== 'undefined', "vuex requires a Promise polyfill in this browser.");
    assert(this instanceof Store, "store must be called with the new operator.");
  }

  var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
  var strict = options.strict; if ( strict === void 0 ) strict = false;

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

通过debugger,我们来看一下每个函数做了哪些事情。

ModuleCollection(options);

ModuleCollection用来初始化 内部变量_modules的值。

installModule

在定义的module中,如果设置了 namespace: true 的话,通过 store.commit('computeSum') 数据的时候要加上路径的上下文。store.commit('cart/computeSum')才可以。

installModule的作用主要是为module加上namespace名字空间(如果有)后,注册mutation、action以及getter,同时递归安装所有子module。

resetStoreVM
/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
function resetStoreVM (store, state, hot) {
  /* 存放之前的vm对象 */
  const oldVm = store._vm 

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  /* 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的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
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent
  /* Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告 */
  Vue.config.silent = true
  /*  这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  /* 使能严格模式,保证修改store只能通过mutation */
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    /* 解除旧vm的state的引用,以及销毁旧的Vue对象 */
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

resetStoreVM首先会遍历wrappedGetters,使用Object.defineProperty方法为每一个getter绑定上get方法,这样我们就可以在组件里访问this.$store.getters.test就等同于访问store._vm.test。

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

之后Vuex采用了new一个Vue对象来实现数据的“响应式化”,运用Vue.js内部提供的数据双向绑定功能来实现store的数据与视图的同步更新。

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

这时候我们访问store._vm.test也就访问了Vue实例中的属性。

通过这一步,我们将state的数据通过new Vue()后,转换为响应式的。同时,将 getters 里面定义的数据通过 new Vue 的computed实现了计算属性的特点,只有当它的依赖值发生了改变才会被重新计算。

利用Vue自己实现 Vuex

我们可以自定义一个 MyStore.js,里面定义项目共享的数据项以及修改数据的方法。然后挂载在根data中。

// MyStore.js
export default class{
  constructor() {
    this.count = 10;
    this.size = 100;
    let self = this;
    this.mutations = {
      incrementCount(){
        self.count = 200;
      },
      incrementSize(){
        self.size++;
      }
    }
  }
}

项目引入:

import MyStore from './MyStore.js';
var app = new Vue({
	el: '#app',
	components: {},
	data: {
    		myStore: new MyStore()
	}
});

这样,我们在组件中可以通过 this.$root.myStore.count 访问到全局变量 count 了。通过 this.$root.myStore.mutations.incrementCount(); 进行变量的修改。

原生 JavaScript 实现 state 状态管理系统

www.cnblogs.com/zhangycun/p… 这篇博客介绍了用 JavaScript 原生代码实现 数据管理的一个方法。利用的是 Proxy + 发布订阅模式。数据改变的时候,通知所有订阅者,render页面。

最后

Vuex中的数据是存储在浏览器内存中的,当刷新页面后会导致数据清空。这个时候要在window beforeunload时候存入 sessionStorage 中,在页面created 或者 beforeCreate 的时候从 sessionStorage 中读取数据。

export default {
  name: 'App',
  created () {
    //在页面加载时读取sessionStorage里的状态信息
    if (sessionStorage.getItem("store") ) {
        this.$store.replaceState(Object.assign({}, this.$store.state,JSON.parse(sessionStorage.getItem("store"))))
    } 

    //在页面刷新时将vuex里的信息保存到sessionStorage里
    window.addEventListener("beforeunload",()=>{
        sessionStorage.setItem("store",JSON.stringify(this.$store.state))
    })
  }
}

blog.csdn.net/guzhao593/a…