开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3天,点击查看活动详情
本篇文章会对vuex的state、getters、mutations、actions
及 useStore
做一个实现。
添加store
首先,vuex对于vue来说是一个状态管理的插件,在vuex4中,插件抛出一个createStore
方法,这个方法内部肯定包含一个install函数,createStore
实际功能也就是帮我们返回store
实例。
创建一个vuex.js文件存放自己实现的vuex
// vuex.js
function createStore(options){
return new Store(options)
}
const Store = function(options){}
Store.prototype.install = function(app){
console.log(app); // 测试插件是否注册成功
app.config.globalProperties.$store = this; // 把store挂载到vue上
}
export {
createStore
}
创建一个store.js写使用vuex的代码。
// store.js
import { createStore } from './vuex' // 引入自己写的vuex.js文件
export default createStore({})
在main中使用store
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App).use(store).mount('#app')
控制台将会打印install
方法中的app
,也就是当前vue实例。这样就实现了store的容器。
接下来对入参的实现。
下文中代码块中的 ...
代表省略了大部分代码,只会保留关键代码。
state
state
是保存状态的地方,接收一个对象。上面的示例中options
就是我们传入store的state、getters、mutations、actions
等等内容。所以可以直接从options
拿到传入的state
。
// vuex.js
const Store = function(options = {}){
this.state = options.state || {}
}
接着随便在state中传入一个参数
// store.js
...
export default createStore({
state: {
count: 0
}
}
这样就可以在组件中使用了
<template>
<p>访问count: {{ $store.state.count }}</p>
</template>
页面上正常显示
看起来state的实现非常简单,但是这肯定不是state的实现,它现在存在一个致命的问题,等实现完getters
再来修改这个问题。
getters
getters
可以认为是 store 的计算属性。它接收一个state的参数,所以需要遍历传入的函数将state传入函数。
// vuex.js
const Store = function(options = {}){
...
this.getters = {}
const getters = options.getters || {}
Object.keys(getters).forEach(key => {
Object.defineProperty(this.getters, key, {
get: () => {
return getters[key](this.state)
}
})
})
}
写一个getters方法
...
getters: {
printCount(state){
return `当前count值为${state.count}`;
}
}
...
在组件中使用printCount
<template>
<p>访问count:{{ $store.state.count }}</p>
<p>调用getters打印count:{{ $store.getters.printCount }}</p>
</template>
也可以正常展示内容
不过开头说到了getters
可以认为是 store 的计算属性,那么测试改一下state,看看getters结果会不会重写计算。
<template>
<p>访问count:{{ $store.state.count }}</p>
<p>调用getters打印count:{{ $store.getters.printCount }}</p>
<p>修改state查看getters是否会改变:<button @click="$store.state.count = 2">修改</button></p>
</template>
执行之后页面显示
当点击修改按钮,count和getters并没有改变过来,这就是上面说的问题,state并不是响应性数据。那么在vue3中,将一个对象置为响应性对象,可以使用reactive
。在vuex中同样可以使用reactive
添加state的响应性。我们首先需要导入vue,从vue中拿到这个方法。
var vue = require('vue');
...
this.state = vue.reactive(options.state || {}) // 响应性
...
这时再去修改count,页面上就能更新了。
mutations
mutations是专门改变state的地方,实现方式与getters类似。不同的是在订阅的时候返回一个函数。触发mutations是使用commit,接收两个参数,mutations名和附加参数。
// vuex.js
const Store = function(options = {}){
...
this.mutations = {}
const mutations = options.mutations || {}
Object.keys(mutations).forEach(key => {
Object.defineProperty(this.mutations, key, {
get: () => {
return (payload) => { // payload就是调用时传递的第二个参数
mutations[key](this.state, payload)
}
}
})
})
...
}
// 将commit方法写到Store的原型上
Store.prototype.commit = function(mutationsName, payload){
this.mutations[mutationsName](payload)
}
添加一个mutations方法
// store.js
...
mutations: {
addCount(state, payload){
state.count += payload;
}
},
...
组件中使用
<template>
<div>
<p>访问count:{{ $store.state.count }}</p>
<p>调用getters打印count:{{ $store.getters.printCount }}</p>
<p>修改state查看getters是否会改变:<button @click="$store.state.count = 2">修改</button></p>
<p>调用执行mutations: <button @click="$store.commit('addCount', 1)">执行</button></p>
</div>
</template>
正常工作没问题
actios
同理,基本实现方式与mutatios保持一致,但是actions函数第一个参数是当前的store上下文,包含store所有的属性及函数。这时在订阅中传入的就是this而不是this.state。
// vuex.js
...
this.actions = {}
const actions = options.actions || {}
Object.keys(actions).forEach(key => {
Object.defineProperty(this.actions, key, {
get: () => {
return (payload) => {
actions[key](this, payload)
}
}
})
})
...
// 同样添加dispath方法
Store.prototype.dispath = function(actionsName, payload){
this.actions[actionsName](payload)
}
添加一个异步更改count的actios
// store.js
...
mutations: {
addCount(state, payload){
state.count += payload;
}
},
...
组件中使用
<template>
<div>
<p>访问count:{{ $store.state.count }}</p>
<p>调用getters打印count:{{ $store.getters.printCount }}</p>
<p>修改state查看getters是否会改变:<button @click="$store.state.count = 2">修改</button></p>
<p>调用执行mutations: <button @click="$store.commit('addCount', 1)">执行</button></p>
<p>调用执行actions: <button @click="$store.dispath('asyncAddCount', 1)">执行</button></p>
</div>
</template>
现在,基本完成了vuex核心概念中的四个的实现。但是现在的vuex只能在模板中和除开setup外其它选项式api中使用,想在setup中使用还得实现一个组合式apiuseStore
。
useStore
useStore函数返回一个store,肯定不能直接返回Store构造函数,这时需要返回构造函数Store的this。可以在install方法中使用provide
方法向vue实例提供一个名为store的属性,值为当前this。然后在useStore函数中返回。
// vuex.js
...
Store.prototype.install = function(app){
app.provide('store', this); // 将store提供到实例中
app.config.globalProperties.$store = this; // 把store挂载到vue上
}
// 定义useStore方法
const useStore = function(){
return vue.inject('store'); // 返回提供的store
}
export {
createStore,
useStore
}
在script setup中使用,并且结合vue的computed和watch监听state的变化。
<script setup>
import { computed } from '@vue/reactivity';
import { watch } from 'vue';
import { useStore } from './vuex'
const store = useStore();
const count = computed(() => store.state.count);
watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal); // 每次更改都将会被监听到
})
</script>
vuex.js文件完整代码
var vue = require('vue');
function createStore(options){
return new Store(options)
}
const Store = function(options = {}){
this.state = vue.reactive(options.state || {}) // 响应性
this.getters = {}
const getters = options.getters || {}
Object.keys(getters).forEach(key => {
Object.defineProperty(this.getters, key, {
get: () => {
return getters[key](this.state)
}
})
})
this.mutations = {}
const mutations = options.mutations || {}
Object.keys(mutations).forEach(key => {
Object.defineProperty(this.mutations, key, {
get: () => {
return (payload) => {
mutations[key](this.state, payload)
}
}
})
})
this.actions = {}
const actions = options.actions || {}
Object.keys(actions).forEach(key => {
Object.defineProperty(this.actions, key, {
get: () => {
return (payload) => {
actions[key](this, payload)
}
}
})
})
}
Store.prototype.install = function(app){
app.provide('store', this); // 将store提供到实例中
app.config.globalProperties.$store = this; // 把store挂载到vue上
}
Store.prototype.commit = function(mutationsName, payload){
this.mutations[mutationsName](payload)
}
Store.prototype.dispath = function(actionsName, payload){
this.actions[actionsName](payload)
}
const useStore = function(){
return vue.inject('store'); // 返回提供的store
}
export {
createStore,
useStore
}
在这个代码中,Object.keys(actions).forEach...
重复率非常高,所以可以优化一下。
var vue = require('vue');
// 提取的公共代码
const forEachValue = (params, target, callback) => {
Object.keys(params).forEach(key => {
Object.defineProperty(target, key, {
get: () => {
return callback(params[key])
}
})
})
}
function createStore(options){
return new Store(options)
}
const Store = function(options = {}){
this.state = vue.reactive(options.state || {}) // 响应性
this.getters = {}
const getters = options.getters || {}
forEachValue(getters, this.getters, (val) => val(this.state))
this.mutations = {}
const mutations = options.mutations || {}
forEachValue(mutations, this.mutations, (val) => {return (payload) => val(this.state, payload)} )
this.actions = {}
const actions = options.actions || {}、
forEachValue(actions, this.actions, (val) => {return (payload) => val(this, payload)} )
}
Store.prototype.install = function(app){
app.provide('store', this); // 将store提供到实例中
app.config.globalProperties.$store = this; // 把store挂载到vue上
}
Store.prototype.commit = function(mutationsName, payload){
this.mutations[mutationsName](payload)
}
Store.prototype.dispath = function(actionsName, payload){
this.actions[actionsName](payload)
}
const useStore = function(){
return vue.inject('store'); // 返回提供的store
}
export {
createStore,
useStore
}