写在前面

很久不写文章了,断了有一年多。这期间加入了新的团队做leader,认识新的朋友,迎接新的挑战。本应该坐下来整理自己的所见与所得,但又觉得是否会起到作用,是否大家会认同。就像以前有一个朋友问我写这么多入门的文章有用吗?还有一些文章好像在自说自话。
其实还真是这样,反观自己的文章,其实大部分是自己写给自己的。
越长大越孤单,很多想法,很多疑问,很多心里话其实是通过文章间接的与自己沟通。换个角度审视自己,是不是在做正确的事情,是不是正确的思想。我也尽可能的想通过自己的文章传达一些技术之外的东西,要明白 “授之以鱼不如授之以渔”。除了技术技巧之外,有什么东西可以受用一生,可以帮助你更快掌握知识点,更快实现目标?我有很多要整理的内容,不放在这里细说,大家可以关注我的公众号《js前端架构》。后面我会整理一下自己做项目管理,团队管理的一些感想,一些思路。

好了,回归正题,今天我们聊一聊 Vuex。看看大家是否真正完全掌握了 Vuex。过去一段时间,我发现大部分同学对技术的理解没有到达一定深度。只是停留在简单的知识点上,并没有深入的去进行思考。这就导致很多知识点其实并没有完全掌握。随便找一个方向进行深入发问,就会触到你的知识盲区。
今天我们就对 Vuex 的各个点进行发问,以一个初学者的角度来重新学习、重新认识下 Vuex。下面的内容中我会用各种问题来进行串联,请大家遇到我的提问时能先简单做下思考。有了结果后再看我的答案。我说的是我的理解,不一定就是对的,希望你也有自己的看法和理解,我的答案只是你心中的答案一个对照。

正文

什么是 Vuex? 在官网的介绍中这样写到:

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

在我们去了解一个技术方案时,第一步应该想清楚这个方案解决什么样的问题?

Q: 那么第一个问题来了, Vuex 是为了解决什么问题呢?是什么样的问题让我们必须要用 Vuex 解决呢?

A:  我们说 Vue.js 是单向数据流模型。在单页面应用中,只允许数据由父元素向子元素进行传递,子元素通过触发父组件事件的方式可以进行数据传递。
想象这样一个场景,如果页面有复杂的组件嵌套,并且嵌套层次可能很深。这种情况下我们的数据需要通过 prop 一级一级向下传递,叶子组件的事件需要一级一级向上触发。这样的方式显然是不好的,我们希望父组件能够更方便的与各个子孙组件进行通信。
再想象一个场景,单页面多路由的情况下,A页面需要传递数据给B页面,我们如何做路由间的数据传递呢?
按照以往的解决方案,一版会选择 storage,cookie或者url携带参数来解决。
这种方案也存在数据安全,数据传递大小限制等问题。
以上的问题有没有一个更优雅的解决方案呢?

Q: 第二个问题来了,如果让你来解决以上的问题,不使用现有框架,有什么好的办法吗?

A: 我们可以在window下设置一个变量,值为一个空对象。可以将所有需要传递的变量数据存储在这个对象中。
通过 Object.defineProperty 对所有属性添加 set 监听,每当属性值发生变化时 通过 window.dispatchEvent 触发一个全局事件。
该事件注册到所有组件中,每当属性变化时通知到各个页面与组件,再由各个组件进行取出单独处理。

我们再看下Vuex,Vuex的核心结构包括:State,Getter,Mutation, Action, Module。使用时通过 new Vuex.Store({…}) 来进行初始化。
我们把 State 看作是那个存储全部需要共享数据的对象。当你需要跨路由,跨组件进行数据共享时,把需要共享的数据保存到 State 中即可。
Vuex 框架会帮你进行同步,只要有修改就会同步到各个组件中。并及时更新页面数据。

数据设置好之后,我们需要关心如何获取,在各个组件中使用 this.$store.state 来获取 state 对象并使用数据。
当你需要修改数据时,你需要触发一个 mutation 来进行数据修改,官网的示例我们看下:

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

每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)
在上面的例子中 type 为 increment
当我们要修改 state 中的 count 属性时,需要这么做: store.commit(‘increment’)
通过触发 mutation 下的 increment 事件来修改
对此,官网也有一句解释:“更改 Vuex 的 store 中的状态的唯一方法是提交 mutation

Q: 第三个问题来了,为什么不能直接通过 this.$store.state.count = 3 来进行赋值?
为什么不能直接对 state 下的数据进行赋值?为什么一定要使用 mutation?

A:  最初提出这个问题时,我听到两个回答是:“mutation 保证数据可溯源” “mutation更明确地追踪到状态的变化”
在官网我找到这么一段话:
”再次强调,我们通过提交 mutation 的方式,而非直接改变 store.state.count,
是因为我们想要更明确地追踪到状态的变化。这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。
此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。有了它,我们甚至可以实现如时间穿梭般的调试体验。“
按照文档内容,上面的两个回答是没问题的。我们可以引申另一个问题:

Q: 数据为什么要溯源?为什么要记录每次状态改变?为什幺要保存状态快照?

A: 首先,通过 this.$store.state.count 进行赋值是没有任何问题的,在非严格模式下这么写不会报错,数据修改可以被监听到。
为什么非要使用 mutation 使数据可溯源呢?试想:
当我在A页面使用 this.$store.state.count = 10; 然后在B页面再次修改 this.$store.state.count = 3;
你是否可以观察到这个修改顺序?你不能,你只能看到修改的结果是 count 值为3,并不知道过程中是谁修改了他。
为了在调试工具中观察到数据变化过程,我们统一使用 mutation 来修改数据,会有一个log系统记录下你的每一次修改,如图:

理解了为什么必须使用 mutation 更新数据后,我们再来看一看 mutation 还有什么问题。 在官网对 mutation 的介绍中有这么一句话:”一条重要的原则就是要记住 mutation 必须是同步函数。“

Q: 为什么 mutation 必须是同步函数? 我在接口请求的异步回调中执行 store.commit(‘increment’) 可以吗?

A:  首先,官网的 mutation 必须是同步函数,是要求 mutation 下的每个事件的回调函数必须是同步代码。
但对 commit 触发事件的时机并没有要求,也就是你可以在任意异步方法中执行 store.commit() 来修改数据
为什么 mutation 的事件回调必须是同步代码呢?其实还是为了保证溯源的准确性,当遇到下面情况时:

store.commit('A');
store.commit('B');
store.commit('B');

我们希望观察数据变化时,log中能体现正确的执行顺序: A -> B -> C
当这其中任意一个方法是异步处理时,就有可能出现偏差,数据失真,加大了调试的复杂度: A -> C -> B / C -> A -> B
所以这就是为什么 mutation 的事件回调必须是同步代码。

除了 mutation 外还有一个修改 state 的办法,就是通过 Action。
Action 类似于 mutation,不同在于:

1. Action 提交的是 mutation,而不是直接变更状态。

2. Action 可以包含任意异步操作。

也就是说 Action 是 mutation 的包装,但是 Action 中可以执行异步代码。 代码示例:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment ({ commit }) {
	  setTimeout(() => {
	    commit('increment')
	  }, 1000)
	}
  }
})

Q: 就因为异步执行代码就发明 actions 吗? actions 和 mutations 为何一个可以异步一个不可以呢? actions 如何保证溯源的准确性呢?

A: 还是那句话,了解一个方案时,要先看这个方案解决什么问题。actions 解决什么问题呢?
试想,一个单页面应用下,在各个页面都要获取一下最新的活动数据,然后在接口请求成功后通过 store.commit(‘updateDialogData’) 来更新全局数据。
这样做是不是很傻,我们不能在一个公共的地方写这个数据请求吗?mutations 下是不可以的。 所以我们提供一个 actions
这样你就可以把之前需要写到各个组件,各个页面的异步代码放到一起统一管理了。通过 this.$store.dispatch(‘xxx’) 来触发事件。
这样的情况下如何保证正确溯源呢?以往我们是通过 mutations 下的 type 来记录历史的。此处同样可以根据 mutations 记录真实的数据变化。
但他不是原始的操作顺序,原始的操作顺序需要按照 actions 的触发顺序记录。

与 actions 的做法类似,当我们每次取出 state 下的数据后都要进行一次格式化,那为什么不能在一个公共的位置统一处理呢?
Getter 就是解决这个问题,内部包含了对 state 下数据的 格式化整理函数。

同理,当state上的数据堆积过多,我们需要划分不同的数据区时,Module 可以更简单的提供这个功能。

总结:

以上问题其实没有任何深度,只是记录了一次内部探讨过程。分享出来与大家共同学习。
我们只需要多思考就可以明白作者为什么要这么设计,这样设计的好处是什么。
但往往我们对一个知识点的深度理解就缺少这么几个问题。
多去发现问题,提出问题,解决问题。这样才能够快速提高,做到知其然知其所以然。
以上,与君共勉。