短短几行代码手写一个 vuex

2,718 阅读5分钟

前言

我们都知道在用 vue 的时候,简单的父子通信和 EventBus 已经不能满足我们的要求了,嵌套层级过多和难以追踪改变是两个较为主要的问题😵,这个时候可以用 vuex 来解决,想必大家都用过,所以今天跟大家分享的是 vuex 的简单实现,真的是超简单,就几行代码(文章结尾有链接),带你领略 vuex 的精髓,并且在最后会有几个问题答疑(比如时光穿梭、本地持久化等)帮大家巩固一下 vuex。

前置知识

vuex 是基于 vue 的状态管理工具,通俗点讲讲就是变量共享,你要知道 this.$store 本质是个对象,大家可以看下下面这张图,看看 this.$store 到底是个啥(只看有标号的行即可)👇: 还记得我们是怎么使用 vuex 的么?

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex); // 这是插件固定写法,没有什么为什么,官网写的很清楚,这样写 vue 会自动调用插件的 install 方法
const store = new Vuex.Store({
  state: {...},
  mutations: {...},
  actions: {...},
  getters: {...},
})
new Vue({
  el: '#app',
  store
})

很显然,Store 里面大致就传入四个参数,目前我们也只需要这些参数就够了,四个参数又可以分为两类,一类是获取数据,一类是修改数据,就像下面这张图画的一样👇: 我感觉上面这张图画的还挺不错😂,应该挺明了。组件和 store 是分开的,组件获取数据可以通过 $store.state$store.getters,组件要修改数据可以通过 $store.actions$store.mutations(当然最终都是 mutations,这也使得便于追踪状态的改变),如此一来,形成了闭环,也符合我们所说的单向数据流的思想,只有一个地方能改数据,不能遍地开花,不然就乱套了。另外 store 也是个单例模式的例子,所有的组件都共用一个全局的 store,每个组件的 $store 都是同一个东西,这点也很好理解。
再补充一句,很多人总是记不清 actions 和 mutations 到底哪个是异步哪个是同步,不妨试试这样记:actions 和 async 都是 a 开头的,所以 actions 是异步,或者 actions 是要调用 api 的,也都是 a 开头的,所以是异步😳。

简单实现

有了上面这些概念,接下来我们就简单用几行代码来简单实现一下吧。首先先写个简单的小框架,就像下面这样(官网上有说明插件的编写方式):

<script src="https://cdn.bootcss.com/vue/2.6.11/vue.js"></script>
<script>
const Vuex = {}
Vuex.install = function (Vue) { // 这个 Vue 是 vue 提供给我们的
  console.log('install 方法开始执行')
}
Vue.use(Vuex)
</script>

ok,然后我们在 install 里面写 vuex 的代码即可,现在我们只需要声明一个 store 对象并且赋予 store 两个属性(这里以 state 和 mutations 举例),这个我用很简单的代码演示一下大家就清楚了👇:

 Vuex.install = function (Vue) {
  console.log('install 方法开始执行')
  const store = {} // 声明一个对象
  store.state = new Vue({ // 赋予 state 属性,用来获取值
    data () {
      return {
        msg: '哈哈'
      }
    }
  })
  store.mutations = { // 赋予 mutations 属性,用来修改值
    SETMSG(value) {
      store.state.msg = value
    }
  }
}

上面的代码中要注意的是 state 其实是利用了 vue 的响应式原理,使用了 new Vue(),由此 state 就变成响应式的了,这是 vue 的特性。注意这里单纯的使用 Object.defineProperty 来定义 state 是不行的,是无法更新视图的,因为它并没有被 vue 进行依赖收集,所以说 vuex 是强依赖于 vue 的。还有就是一开始我们也要把 state 的数据写全,不然后面添加的也是无响应更新的,大家如果有用到过 $set 应该能体会到为什么有时候数据变了,视图没更新那种感觉😬。

👌,现在我们已经有了 store,接下来就是把它挂到每个组件下面了,这里我们使用 Vue.mixin 的混入方式,代码如下:

Vue.mixin({ // 也是固定写法:每个组件都会执行下面这个生命周期
    beforeCreate () {
      this.$store = store // 于是每个组件都会有 this.$store,并且都指向同一个 store
    }
  })

当然用 Vue.prototype.$store = store 也能达到同样的效果,事实上,vue-router 也是同样的方式我们才能在每个实例中用 this.$router 来调用。另外如果要说 mixin 和 prototype 这两种挂载方式的区别,我就想到两小点😯:

  • mixin 是在 vue 实例上,prototype 是在原型上
  • 把 store 挂载在实例上,就不用顺着原型链查找了,提升了一丢丢性能

不知道大家还知道其他原因吗,欢迎在下面留言。

好了,至此,我们大概就写完了一个简单的 demo,现在写个例子来测试一下:

let v1 = new Vue({
  el: '#component1',
  computed: {
    data() {
      return this.$store.state.msg
    }
  }
})
let v2 = new Vue({
  el: '#component2',
  methods: {
    change() {
      this.$store.mutations.SETMSG('这是 mutations 触发的值')
    }
  }
})
console.log(v1, v2)

下面是测试的结果: 当然我们平时用 mutations 是通过 commit 来写的,其实 commit 就是个函数,本质上也是调用 mutations,这里也顺手简单写下 commit👇:

Vuex.install = function (Vue) {
    ...
    store.commit = function(mutationName, value) {
        store.mutations[mutationName] && store.mutations[mutationName](value)
    }
    ...
}
...
let v2 = new Vue({
  el: '#component2',
  methods: {
    change() { // 改一下调用方式,结果是一样的
      // this.$store.mutations.SETMSG('这是 mutations 触发的值')
      this.$store.commit('SETMSG', '这是 mutations 触发的值')
    }
  }
})

最终效果是一样的,这里就不展示了。 深吸一口气,目前为止我们已经实现超简版的 vuex,接下来是几个问题答疑🤔。

问题答疑

为什么需要 getters

这个东西其实和我们平时写的计算属性一毛一样,state 和 getters 的关系好比 data 和 computed,大家细品一下。

如何区分 state 是不是通过 commit 修改的

我们知道 vuex 中修改 state 就一个通道,就是执行 commit,但其实你不通过 commit 也是能改的,那怎么知道它是通过 commit 修改的呢?就是在执行 commit 的时候加个标志位 _committing,执行 commit 的时候将 _committing 设置为 true,_committing 为 true 才能修改 state,而其他方式修改的 state 并不会修改 _committing 标志位,这样一来就能判断是不是通过 commit 修改的。 如果你在 vuex 中打开了严格模式,任何非 mutation 更改都会抛出错误。

mapState 等辅助函数的实现

这一类辅助函数本质就是语法糖,这里我们以 mapState 举例子,我们回顾一下用法:

import {mapState} from 'vuex'
export default{
    computed:{
        ...mapState(['msg','user'])
    }
}

然后我们在页面中就能用 this.msg 访问,其实调用的还是 this.$store.state.msg,只不过写起来简单点。下面我们看下怎么简单实现,很显然,这里 mapState 是一个函数,接收一个数组:

function mapState (list) {
  let obj = {}
  list.forEach(stateName => {
    obj[stateName] = () => this.$store.state[stateName]
  })
  return obj
}

👏是的,就这么点代码,其他 map 辅助函数也是也一样的道理。

本地持久化

  • 我们可以在每次 commit 的时候把 state 保存到 localStorage 或者 sessionStorage 中,然后页面初始化的时候,先读取本地存储的 state 值,不过要注意频繁存储的问题。
  • 我们可以利用 beforeunload 这个事件,在页面卸载之前再把 state 的值存起来,这样效率也挺高的。

时光穿梭

这是 devtoolPlugin 提供的功能,因为开发的时候所有 state 的改变都有记录,“时光穿梭”的功能其实就是能够让我们回到或去到某一状态,实际上就是将当前的 state 替换为记录中的某个 state,我们看下 vuex 的 store 中就为我们提供了一个这样的函数 replaceState(真的是直接替换😂),具体代码如下👇:

replaceState (state) {
    this._withCommit(() => { // 这里面就是上面说到的 _commiting 标识
      this._vm.state = state
    })
  }

下面是两个个小截图,希望能够帮助你理解:

小结

如果硬要说 vuex 中最核心的一个点的话,那就是利用了 vue 的响应式,我想这是最为主要的。最后希望本篇文章能够对你有所帮助,不知道写的清不清楚😁,也祝大家百毒不侵,开开心心上班,回见👋。

ps: 最简版 vuex 代码地址精装版 vuex 代码地址