0x0 Vuex 基础 - Vuex 是什么?

549 阅读7分钟

Vuex 基础教程系列 🎉

概述

Vuex 是专门为 Vue.js 开发的用于全局状态管理的状态管理库。它是对 Flux 架构的实现工具之一,类似的例如 redux。与 redux 不同的是 Vuex 中存储的状态具有响应式特性。

【何为全局状态?】

  • 多个组件共享一个状态。
  • 一个状态会被多个组件变更。

通常而言,对于跨组件的状态共享和变更,我们常用的手段主要有以下几种:

  • 父子孙组件层层传递、显得会很繁琐。
  • 兄弟组件会将共享状态抽离到父级组件中,并通过事件来变更状态。
  • 不相邻组件或者是具有跨越层级较多的父组件,一般就会将状态直接挂载在 Vue 的根应用实例上。

不论以上的那种方式,它们都有自己的繁琐性、模式的脆弱性以及带来的代码难以维护灯问题。

【Vuex 的全局状态管理思想】

总之、如果与组件层级具有耦合关系,那么共享状态的管理便具有局限性。为此,Vuex 采用了集中式存储管理应用所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

简而言之、就是将会被多个组件共享的状态抽取出来,以一个全局的单例模式进行管理,在这种方式下,组件树构成了一个巨大的视图,但是不论树的那个节点,都可以轻易的从全局单例的状态管理中读取或变更状态。

通过将“视图”与“状态”强制隔离,从而让我们的代码变得更结构化且易维护,同时也会让我们的应用更加健壮。

从一个例子走起!

以“ Vuex 的思想”为灯塔,让我们来一步步走近它。

在学习 Vue 基础的时候我们说过 Vue 应用的根实例的 data 属性可以是一个对象,但是普通组件的 data 属性必须要是一个函数,其原因就是防止错误的用法将一个状态被多个组件共享,从而产生意料之外的影响。

现在,基于 Vuex 的全局状态管理思想,我们要反其道而行,主动将多个组件共享的状态抽离出来,进行集中式管理:

const blogData = {
    title: 'blog title',
    post: 'blog content'
};

Vue.component("blog-item", {
    name: 'BlogItem',
    data() {
        return { blogData }
    }
});

Vue.component("blog-content", {
    name: 'BlogContent',
    data() {
        return { blogData }
    }
});

现在如果有其它的组件或者行为更改了 blogData 中的值,那么 blog-titleblog-content 都将自动地更新它们的视图。

虽然我们通过非常简单的方式实现了跨组件、多组件的数据共享,但是这也为调试带来了噩梦,因为我们的应用在任何时间、任何部分都可能会变更 blogData 的值,我们无法准确的定位正在发生变更的地方,因此也无法留下变更过的记录。

为了解决这一问题,我们必须对共享的数据进行规范,例如采用一个简单的 store 模式


var store = {
    debug: true,
    state: {
        blogData: {
            title: 'blog title',
            post: 'blog content'
        }
    },
    setBlogDataAction(newValue) {
        if (this.debug) console.log('setMessageAction triggered with', newValue)
        this.state.blogData = { ...this.state.blogData, ...newValue }
    },
    clearBlogDataAction() {
        if (this.debug) console.log('clearMessageAction triggered')
        this.state.message = {
            title: '',
            post: ''
        }
    }
}

现在 store 中的 state 变更,必须要基于自生的 action 去管理,这样的好处就是有一个统一的状态管理方法,我们可以在这个方法中基于状态变更的前后提供更多的扩展能力,例如记录状态变更的日志。

注意,你不应该在 action 中 替换原始的状态对象 - 组件和 store 需要引用同一个共享对象,变更才能够被观察到。

此外,每个实例/组件仍然可以拥有和管理自己的私有状态。

Vue.component("blog-item", {
    name: 'BlogItem',
    data() {
        return {
            blogData: store.state,
            privateState: {}
        }
    }
});

Vue.component("blog-content", {
    name: 'BlogContent',
    data() {
        return {
            blogData: store.state,
            privateState: {}
        }
    }
});

下图是对上述方式的图解:

image.png

上面的方式还有一个很严重的缺陷,那就是我们还需要手动将状态赋值给组件的 data 属性,从而让组件实例初始化时将其进行代理然后转变为响应式对象。

要想解决这个问题,我们可以使用 Vue 提供的了另一个 API Vue.observable ,它赋予了我们自定义创建独立于组件之外的响应式数据的能力。

import Vue from "vue";

let num = 0;
const store = {
  state: Vue.observable({ count: 0 }),
  setState(action, payload) {
    if (action in this.state) {
      this.state[action] = payload;
    }
  },
};

const CompA = Vue.extend({
  name: "CompA",
  render(c) {
    return c(
      "button",
      {
        on: {
          click: function () {
            store.setState("count", ++num);
          },
        },
      },
      ["click"]
    );
  },
});

const CompB = Vue.extend({
  name: "CompB",
  render(c) {
    return c("div", [store.state.count]);
  },
});

export default Vue.extend({
  name: "HelloWorld",
  components: { CompA ,CompB},
});

当然 Vuex 的官方源码中并没有使用这种方式,Vuex 的实现思路非常简单,通过重新创建一个 Vue 实例,借助组件实例来初始化响应式数据,将传入的 state 转变为响应式对象。

var oldVm = store._vm;

store._vm = new Vue({
    data: {
        $$state: state //此处的 state 就是我们 new Vuex.Store({state:{...}}) 时传入的值。
    },
});

if (oldVm) {
    //会在下一个 tick 将这个实例销毁。
    Vue.nextTick(function () { return oldVm.$destroy(); });
}

以上便是对 Vuex 集中式管理状态思想的非常粗暴简单的实现,但是思想内核是一致的,旨在加深我们对 Vuex 的认识。

来自 Vuex 的约定

Vuex 会以相应的规则保证状态按照一种可预测的方式发生变化,那么这里的规则便是我们要说的“约定”:

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而能让一些工具帮助我们更好地了解我们的应用,例如保存状态快照以及时间旅行等。
  • 异步的状态变更必须要在 action 中 (commit) mutation。并使用 dispatch 来分发 action。

Vuex 的周边

  1. Vuex 与 Vue-devtools 结合使用,可以提供更高级的调试功能,例如时间旅行(time travel)、状态快照导入导出等。
  2. vuex-persistedstate :Vuex 的持久化插件,它会使用浏览器的本地存储持久化保存 state,这在一些特殊场景下非常有用,例如购物车、表单填入的值。
  3. vuex-shared-mutations :持久化存储并可以跨浏览器标签同步状态。
  4. vuex-i18n:允许你轻松地用多种语言存储内容。让你的应用切换语言时更容易。

什么时候使用 Vuex?

Flux 架构就像眼镜:您自会知道什么时候需要它。 ——Dan Abramov 【redux 作者】

具体来说就是要评估自己产品或项目的大小程度,如果是小型项目,一个简单的 store 模式就足够您所需,但是对于中大型应用,那么非常推荐使用 Vuex 来跨组件层级的方式进行状态管理。

Vuex 还有存在的必要吗?

随着 Vue3 的发布,在见识了 Composition API 强大的灵活性,有很多人开始质疑 Vuex 是否还有存在的必要性。对此,我个人的看法是需要使用 Vuex 解决问题的场景从未改变:

理由很简单,可以简单的概括为以下几点:

  • 早在 Vue2.x 版本 Vue 就已经暴露了其底层的响应式系统,例如 Vue.observable API,利用它我们可以手动的创建响应式数据。
  • Vuex 不单单只是状态管理,它还可以记录日志、追踪变更。
  • 活跃的社区,众多开发者的参与,让它更稳定、更可扩展、更强大,更可靠,符合大型应用的状态管理,这是个人封装或小团队封装的状态管理库难以企及的。
  • 丰富的周边配套,例如一些状态持久化插件,并且结合 vue-devtools 可以获得更好的调试体验,比如时间旅行,再比如状态快照导入导出。

还是那句话,小型项目一个简单的 store 模式或者借助 Vue.observable API 即可,但是中大型项目,则强烈建议使用 Vuex。