Vuex和Redux的本质

2,001 阅读9分钟

前言

  Vuex和Redux是前端常用的状态管理库。Vuex和Vue深度集成,必须得配合Vue进行使用。Redux是独立的,并没有和哪个框架耦合,但一般配合React使用。本篇文章并不是讨论Vuex和Redux的优劣,更何况我认为这两者本质上是一样,本篇文章便是谈谈Vuex和Redux的本质(我不敢肯定我已经接触到了本质😁)。Vuex和Redux我都是有使用经验的,这两者的源码都大致看过,最后得出一个结论:它们本质上都是管理全局变量的机制。

正文

  下面将会从状态管理库诞生的起因,到Vuex和Redux的设计思想,最后稍微深入介绍Redux和Vuex。

为什么需要状态管理库?

  存在即真理,一个事物存在必然有其必要性,为什么需要状态管理库?假设没有状态管理库?我们的代码会怎样呢?假设有下面的组件结构:

根组件是App, App由两个组件组成 Home和Task,这两个组件又分别由Header和Body,List和Detail组成。现在假设存在以下组件间共享变量的情况:

  • App组件需要和Task组件共享变量。很简单!直接在App组件内定义一个变量,然后通过prop传递给Task组件。
  • Home和Task要共享变量。嗯,没问题的,还是在App内定义变量,然后通过prop传递到Home和Task,变量的修改都通过事件冒到App组件。
  • Header和List要共享变量。头大了,我还是得在App内定义变量,然后传递给Home和Task,再传给Header和List,修改的时候也得每一层都冒出事件。

  看到了这里大家都可以看出问题了,随着组件层级的加深,组件间共享变量难度将会加大,中间的组件即便我不需要这个变量,我还是得接受并往子组件传递,这些都需要使用代码实现。在修改的时候,我要查看组件变量的来源,我得一层层往上查找。而这些问题还会随着组件层级的加深加剧。

  有些同学可能会提出,在React上使用context,我把这些变量都定义在App组件的context上,通过context实现共享;Vue上我可以使用provide和inject。这些仅能实现部分功能,通过context我们的确可以把共享的值传递给所有的子组件,但是在vue和React都是单向数据流,这也就是说子组件无法改变共享的值,如果还继续通过回调来让根组件改变共享的值,那这个方案和prop是一致的,无法解决我们的问题。

  还有同学提出,可以使用全局变量,直接在一个模块建共享的变量,然后各个组件直接使用或者改变这个模块的变量,或者直接在window下挂全局变量。无疑,通过这些,是完全可以实现变量的共享的,但提供了一个谁都可以改变的全局变量无疑是灾难。变量在某个组件被修改了,导致在其它组件出了问题,我们无法知道在哪里修改,很难找到现场,这会导致错误排查异常艰难,这些问题正是状态管理库出现的原因。

  还可以使用EventBus,EventBus配合Context的确可以解决问题,但使用EventBus与全局变量存在同样的问题。在一些共享级别没那么高的时候使用EventBus去解决问题反而是比较简单的,但在共享级别比较高的时候,使用EventBus便会面临着和全局变量一样的困境。

Vuex和Redux的设计思想

vuex和Redux本质上都是建立一套让开发者对全局变量的修改有意识的机制,这套机制为了让开发者意识到自己在做一些重要的、影响较大的操作,所以加入了一些繁琐的操作,这是状态管理库的基本设计思想。我认为状态管理库必须有以下特点:

  • 必须能够且易于共享数据,它的最基础功能就是提供共享的状态。
  • 提供一套机制用于修改状态,这套机制必须能让开发者意识到自己在修改状态。
  • 相同状态对应相同视图,不同状态对应不同的视图。

全局变量

  前面曾说过状态管理库中状态本质上也是全局变量,那下面用全局变量模拟一下。直接在用vue进行模拟,不使用vuex,直接在store/index.js中

const shared = {
  test: 1,
  other: 2
}
export default shared

有两个vue组件,App和HelloWorld

App.vue

<template>
  <div id="app">
    <h2>App Component</h2>
    <div>store={{store}}</div>
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
import store from './store';

export default {
  name: 'app',
  components: {HelloWorld},
  computed: {
    store() {
      return store;
    }
  },
}
</script>
<style>
#app {
  border: 1px solid red;
  height: 500px;
  padding: 50px;
}
</style>

HelloWorld.vue

<template>
  <div class="hello">
    <h2>HelloWord Component</h2>
    <button @click="changeStore">改变store的值</button>
    <div>store.other={{other}}</div>
    <div>store={{store}}</div>
  </div>
</template>
<script>
import store from '../store';

export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
  computed: {
    other() {
      return store.other;
    },
    store() {
      return store;
    }
  },
  methods: {
    changeStore() {
      store.test = 88888;
      this.$parent.$forceUpdate();
      this.$forceUpdate()
    }
  }
}
</script>
<style scoped>
  .hello {
    height: 200px;
    margin: 50px;
    padding: 50px;
    border: 1px solid blue;
  }

</style>

一开始时的视图:

点击“改变store的值”按钮后的视图:

可以发现在一个全局变量模块内定义全局变量确实能够达到不同组件共享变量效果。

状态改变触发视图更新

前面例子的全局变量改变后并不会触发更新,前文之所以看到视图更新是使用了vue的强制更新api。

 this.$parent.$forceUpdate();
 this.$forceUpdate()

事实上状态改变触发视图更新不应该是状态管理库做的事情,它所做的是应该仅仅是提供共享的数据和提供一种改变状态的机制。

redux的确是这样做的,它状态的改变是无法触发组件的重新计算,它本身是独立的,和具体框架无关的,但是它提供了订阅state改变的方法,在store实例上存在一个subscribe方法,该方法允许传入一个函数,在每次state改变时都会回调改方法。于是react-redux利用改方法实现了state改变触发组件重新计算。

而vuex则不是这样的,vuex是与vue高度集成的状态管理库,这里的高度集成的意思是,使用vuex的时候必须使用vue,这是因为vuex内部本就存在一个vue实例,vuex触发视图更新就是使用了vue实例,当所有的共享状态都存在在vue实例的data选项上,那么在共享变量改变时自然会触发视图的更新。

改变状态的机制

  前文一直在强调状态管理库提供一种用于改变状态的机制,下面聊聊这套机制。无论是Redux或Vuex的做法都大同小异,无疑都是把状态改变的操作搞的繁琐一点。

Redux要求开发者必须定义Reducer,并且Reducer必须是纯函数(见下文),Reducer是用来执行改变state的函数,所有state的改变都必须在Reducer中执行,Redux还提供了一个dispatch方法,该方法用于发起状态的修改,所有的状态的修改都是直接或间接通过dispatch发起,最终在reducer中执行,而dispatch通过action指向具体的reducer。 Redux是通过action来让开发者意识到自己的操作,每一种action都对应一种修改状态操作,也对应着一个reducer。下面看一个例子:

import { createStore } from "redux";
// reducer
function countRucer(state, action) {
  switch (action.type) {
    case "COUNT_ADD":
      state.count += action.value;
      break;
    case "COUNT_MINUS":
      state.count -= action.value;
      break;
    default:
      return state;
  }
  return { ...state };
}

const store = createStore(countRucer, { count: 0 });
// 获取state
console.log(store.getState());
// { type: "COUNT_ADD", value: 8 } 为一种action
store.dispatch({ type: "COUNT_ADD", value: 8 });
store.dispatch({ type: "COUNT_MINUS", value: 4 });

这里是最简单的redux使用例子,redux当然还提供很多高级用法,比如reducer的合并,enhancer等等。redux并没有集成异步操作的解决方案,但是它提供了中间件接口,开发者可以自行开发中间件使其支持异步action。所谓的中间件便是在dispatch到reducer执行的中间加入一些代码完成一些操作。

在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数:
1、此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
2、该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

vuex改变状态的机制也是大同小异,vuex中的mutation和reducer一样,所有的状态的改变都应该在mutation中执行,开发者通过commit发起操作,commit的第一个参数是mutation的名字。下面看看例子:

import {Store} from 'vuex';

const store = new Store({
  state: {
    count:  0
  },
  mutations: {
    COUNT_ADD(state, value) {
      state.count += value
    },
    COUNT_MINUS(state, value){
      state.count -= value
    }
  }
});

console.log(store.state)

store.commit('COUNT_ADD', 10)
store.commit('COUNT_MINUS', 10)

与redux不同的是,vuex集成了异步解决方案,vuex加入了action用以进行异步操作,必须提到的是action是非必须的,但使用它可以很好组织代码,让业务聚合。action是通过dispatch发起,dispatch的第一个参数是具体一类action的名字,在action中必须要通过commit发起mutation或者dispatch另外一个action,通过另外一个action发起mutation。可以确定的是状态的改变必须在mutation中进行,下面看一个简单的例子:

import {Store} from 'vuex';
import api from '@/api'

const store = new Store({
  state: {
    taskList:  {}
  },
  mutations: {
    GET_TASKS_PENDING(state) {
      state.taskList = {
        data: null,
        isPending: true,
        isReject: false,
        isFulfill: false,
      }
    },
    GET_TASKS_FULFILL(state, data) {
      state.taskList = {
        data,
        isPending: false,
        isReject: false,
        isFulfill: true,
      }
    },
    GET_TASKS_REJECT(state, error) {
      state.taskList = {
        data: null,
        isPending: false,
        isReject: true,
        isFulfill: false,
        error,
      }
    },
  },
  action: {
    async getTask({commit}) {
    // 这里是一种特殊的写法,在action的三个阶段都会commit一个mutation,
    // 最后让state带上状态
      try {
      commit('GET_TASKS_PENDING')
      const data = await api.getTasks()
      commit('GET_TASKS_FULFILL', data)
      } catch(e) {
        commit('GET_TASKS_REJECT', e)
      }
    }
  }
});

store.dispatch('getTask')

下面是尤大大关于action的解释:

总结

  本文并不是一篇关于如何使用vuex或redux,或者对这两者进行对比的文章,本文更着重于这两者背后异曲同工的设计思想。本文想告诉读者的是:在学习redux的时候不要纠结reducer、action和dispatch是什么,在学习vuex的时候也不要苦恼于为什么有mutation又有action,它们的本质都是改变state的机制,就是用来改变状态管理库中的一个变量的。它们就是用来把修改变量这个简单的操作变得麻烦,至于为什么要让这个操作变得麻烦?这是因为状态管理库中的变量是用以共享的,它的改变影响到众多组件。状态管理库要记录这些操作以帮助你在开发时排除错误和让你意识到你在做一个重大的操作。