手写 vuex4.0 实现原理

在这里插入图片描述

首先我们来看一个小小的vuex4.0使用案例

App.vue

<template>
  <div> count: {{ count }} | double: {{ double }} </div>
  <!-- $store 是挂载到实例上,兼容 vue2 用的 -->
  <div> $store.state.count: {{ $store.state.count }} </div>
  <hr>
  <button @click="add">同步修改</button>
  <button @click="asyncAdd">异步修改</button>
</template>

<script>
import { computed, toRefs } from 'vue'
import { useStore } from 'vuex'

export default {
  name: 'App',
  setup() {
    const store = useStore('store1')
    function add() {
      store.commit('add', 1)
    }
    function asyncAdd() {
      store.dispatch('asyncAdd', 1)
    }

    return {
      add,
      asyncAdd,
      // ...toRefs(store.state)
      // 通过 computed 监听实现响应式,因为 count: store.state.count 就变成赋值了不具有响应式功能
      count: computed(() => store.state.count),
      double: computed(() => store.getters.double)
    }
  }
}
</script>

store/index.js

import { createStore } from 'vuex'

const store = createStore({
  state: {
    count: 0
  },
  getters: {
    double(state) {
      return state.count * 2
    }
  },
  mutations: {
    add(state, payload) {
      state.count += payload
    }
  },
  actions: {
    asyncAdd({ commit }, payload) {
      setTimeout(() => {
        commit('add', payload)
      }, 1000)
    }
  }
})

export default store

main.js

import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
// Vue.use(store) 插件的用法, 会默认调用store中的install方法
createApp(App).use(store, 'store1').mount('#app')

手写一个 vuex4.0 需要实现以上功能,那么我们先从 main.js 出发,createApp(App) 会返回一个 vue 实例,其 use 方法接收一个含有 installstore 对象,并执行 install(vue, 'store1') vue实例'store1' 作为参数,将 store 对象内的数据注入 vue 中。

那么 store 对象是怎么得到的呢?再看 store/index.js 我们可以发现 store 是通过
createStore({}) 得到的, 则由此看来 createStore 是一个函数接收一个对象作为参数并返回 store对象里面包含一个 install 方法。

【vuex 0.1版本 实现如下】

export function createStore(options) { // options 是传入的配置
  // todo options
  return {
      install(vue, injectKey) {}
  }
}

讲道理 createStore 方法需要对传入的 options 进行一些复杂的处理才能返回一个功能更全面的 store ,那么我们就写一个 class 来处理 options 吧!
这里还有一个知识点 vue3 中可以导出 import { provide, inject } from 'vue'
provide, inject 两种方法的官方说明:
父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据
想想这是不是可以拿来存储 vuex 的数据

【vuex 0.2版本 实现如下】

import { inject } from 'vue' // inject:获取store数据

export function createStore(options) {
  return new Store(options)
}

const storeKey = 'store'
export function useStore(injectKey = storeKey) { // 获取 vuex 内的数据
  return inject(injectKey)
}

// 这个 Store 类就是用来处理 options 的
class Store {
  constructor(options) {
    // todo options
  }
  install(vue, injectKey = storeKey) {
    // vue3 的 provide 方法将数据存入 _provides 需要时通过 inject 获取
    vue.provide(injectKey, this) // injectKey 为存入名称,取时通过 inject(injectKey) 取
    vue.config.globalProperties.$store = this // 将 $store 加到 vue3 实例上 (兼容 vue2)
  }
}

store/index.js 内注入 state 数据,在 App.vue 中我们可以看到 vuex 数据通过 const store = useStore('store1') 导出,通过 store.state.xxx 获取,则我们只需通过 reactive
(vue3 中的 reactive 方法可使传入的对象具有响应式功能) 让传入的 state 数据具有响应式

【vuex 0.3版本 实现如下】

import { inject } from 'vue' // inject:获取store数据

export function createStore(options) {
  return new Store(options)
}

const storeKey = 'store'
export function useStore(injectKey = storeKey) { // 获取 vuex 内的数据
  return inject(injectKey)
}

class Store {
  constructor(options) {
    const store = this // 把 this 写成 store 方便理解
    // 多一层 data 是为了解决 store._state.data = xxx 整个重新赋值导致失去响应式问题
    store._state = reactive({ data: options.state })
  }
  // 实现外部可通过 store.state 获取响应式数据
  get state() {
    return this._state.data
  }
  install(vue, injectKey = storeKey) {
    // vue3 的 provide 方法将数据存入 _provides 需要时通过 inject 获取
    vue.provide(injectKey, this) // injectKey 为存入名称,取时通过 inject(injectKey) 取
    vue.config.globalProperties.$store = this // 将 $store 加到 vue3 实例上 (兼容 vue2)
  }
}

那 vuex 中的 getters 又是怎么实现的呢? 在 store/index.js 中我们发现 getters 是一个对象里面放的都是函数,可是在 App.vue 中我们是通过 store.getters.double 直接获取值,而不是 store.getters.double() 来获取返回的值,那这又是怎么实现的呢? 想想是不是可以通过 Object.definePropertyget 方法,当触发 get 时,我们自动执行 double() 方法直接返回值呢!

【vuex 0.4版本 实现如下】

import { reactive, inject } from 'vue' // reactive:响应式 | inject:获取store数据

export function createStore(options) {
  return new Store(options)
}

const storeKey = 'store'
export function useStore(injectKey = storeKey) { // 获取 vuex 内的数据
  return inject(injectKey)
}

class Store {
  constructor(options) {
    const store = this
    // 多一层 data 是为了解决 store._state.data = xxx 整个重新赋值导致失去响应式问题
    store._state = reactive({ data: options.state })
    
    // 实现 getters 其实就是实现一个计算属性
    const _getters = options.getters
    store.getters = {}
    forEachValue(_getters, function (fn, key) {
      Object.defineProperty(store.getters, key, {
        enumerable: true, // 可枚举
        get: () => fn(store.state) // 用 computed 包裹可以实现状态缓存
      })
    })
  }
  // 实现外部可通过 store.state 获取数据
  get state() {
    return this._state.data
  }
  install(vue, injectKey) {
    // vue3 的 provide 方法将数据存入 _provides 需要时通过 inject 获取
    vue.provide(injectKey, this) // injectKey 为存入名称,取时通过 inject(injectKey) 取
    vue.config.globalProperties.$store = this // 将 $store 加到 vue3 实例上 (兼容 vue2)
  }
}

// 此公共方法用于遍历获取对象的 value 与 key 传入回调函数
function forEachValue(obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

接下来就是实现 dispatchcommit 了,实现的关键点就是传入的 第一个参数 与它们执行时函数的 this 指向问题。这里我们用到了 () => {} 箭头函数 this 指向当前作用域的特性 与 js的 call 方法改变函数内部的 this 指向。

青铜版本 vuex4.0

import { reactive, inject } from 'vue' // reactive:响应式 | inject:获取store数据

export function createStore(options) {
  return new Store(options)
}

const storeKey = 'store'
export function useStore(injectKey = storeKey) {
  return inject(injectKey)
}

// 创建容器返回一个 store
class Store {
  constructor(options) {
    const store = this
    // 解决重新赋值问题 store._state.data = xxx
    store._state = reactive({ data: options.state })

    // 实现 getters 其实就是实现一个计算属性
    const _getters = options.getters
    store.getters = {}
    forEachValue(_getters, function (fn, key) {
      Object.defineProperty(store.getters, key, {
        enumerable: true,
        get: () => fn(store.state) // 用 computed 包裹可以实现状态缓存
      })
    })

    // 实现 dispatch commit
    const _mutations = options.mutations
    const _actions = options.actions
    store._mutations = Object.create(null) // 我们为什么不用 {} 来创建一个空对象呢,这样不是更简单吗? 你错了 {} 创建的对象有原型链 性能低
    store._actions = Object.create(null)
    forEachValue(_mutations, function (mutation, key) {
      store._mutations[key] = (value) => {
        mutation.call(store, store.state, value) // call 方法将 mutation 函数的 this 指向 store
      }
    })
    forEachValue(_actions, function (action, key) {
      store._actions[key] = (value) => {
        action.call(store, store, value)
      }
    })
  }
  
  get state() {
    return this._state.data
  }
  
  // 箭头函数的目的是让 this 指向当前作用域,也就是本 Store 类:解决如:const { dispatch, commit } = useStore() 结构出来的 this 指向问题
  dispatch = (type, value) => {
    this._actions[type](value)
  }
  commit = (type, value) => {
    this._mutations[type](value)
  }
   
  // vue3 接收 install 方法为项目注入此 store 库,app 为引入的 vue3 根实例
  install(vue, injectKey = storeKey) {
    // vue3 的 provide 方法将数据存入 _provides 需要时通过 inject 获取
    vue.provide(injectKey, this) // injectKey 为存入名称,取时通过 inject(injectKey) 取
    vue.config.globalProperties.$store = this // 将 $store 加到 vue3 实例上 (兼容 vue2)
  }
}

// 此公共方法用于遍历获取对象的 value 与 key 传入回调函数
function forEachValue(obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

好了,大功告成!

CodeSandbox 效果演示

https://codesandbox.io/s/vuex4-70mgk?file=/src/App.vue
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值