首先我们来看一个小小的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
方法接收一个含有 install
的 store
对象,并执行 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.defineProperty
的 get
方法,当触发 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))
}
接下来就是实现 dispatch
与 commit
了,实现的关键点就是传入的 第一个参数
与它们执行时函数的 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 效果演示