Vue系列深入教程(二)-- 实现基本的vuex
大家好,我是小黑。
本人打算写一个和vue全家桶相关的系列教程,从vue2到vue3,这系列不会有太多讲解vue全家桶的使用,所以需要对vue及其全家桶比较熟练。
系列文章
- Vue系列深入教程(一)-- 实现基本的vue-router
- Vue系列深入教程(二)-- 实现基本的vuex
建议系列文章从头读起,不要跳着读。
本文使用vue2基于hash实现简单的vuex,因为是vue2写的,所以实现vuex的版本也是3版本。
系列教程代码menorepo仓库地址:gitee.com/jimpp/front…
本节代码在vue-series-demo/my-vuex下
Vuex使用回顾
1. npm install vuex; (这个必须的)
2. 在main.js中
import Vuex from 'vuex'
Vue.use(Vuex)
3. 添加store.js
const store = {
state: {
count: 1
},
getters: {
doubleCount(state) {
return state.count * 2
}
},
mutations: {
setCount(state, count) {
state.count = count
}
},
actions: {
setCount({commit}, count) {
commit('setCount', count)
}
}
}
export default store
4. 初始化store
import staticStore from './store.js'
const store = new Vuex.Store(staticStore);
5. 初始化vue实例的时候注入store
new Vue({
el: '#app',
store, // 在这里注入了
render: c => c(App),
})
6. 注入成功后,所有组件都可以访问$store对象,能够访问store实例
7. 在.vue文件中
<template>
<div>
<button @click="$store.commit('setCount', 4)">mutations</button>
<button @click="$store.dispatch('setCount', 2)">action</button>
<div>count: {{$store.state.count}}</div>
<div>double:{{$store.getters.doubleCount}}</div>
</div>
</template>
8. 点击两个按钮可以看到界面发生变化
vuex的实现思路
store的特点
- Vuex必定是一个包含Store class导出的类,因为可以通过new Vuex.store实例化
- 每个实例都会有state, getters, mutations, actions等属性
- state可以直接读取,但是不能直接通过store.state = xxx赋值
- 每个实例都会有commit, dispatch方法
- state所有数据是响应式的
- 实例化后,原型会挂载$store对象
- Vue.use方法的原理,不懂的同学看上一篇文章
关键就在于实现响应式的state,state可以有两种方法实现,但是其实都是基于vue框架的
state第一种实现
通过new 一个 vue实例实现
const state = new Vue({
data: {
intervalStore: {}
}
});
这样state中的属性必定是响应式的
state第二种实现
通过Vue.observable(object), 不知道的同学请看上一篇文章
const state = Vue.observable({
instarvalStore: {}
})
这样state中的属性也是是响应式的
环境准备
本章不会帖太多环境代码,大部分跟上一章一样,只贴关键的代码
目录结构
依旧是使用vite
package.json
{
"name": "my-vuex",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"vue": "2.7.9"
},
"devDependencies": {
"vite": "3.0.7",
"vue-template-compiler": "2.7.9",
"vite-plugin-vue2": "1.9.3"
}
}
app.vue
<template>
<div>
<button @click="$store.commit('setCount', 4)">mutations</button>
<button @click="$store.dispatch('setCount', 2)">action</button>
<div>count: {{ $store.state.count }}</div>
<div>double:{{ $store.getters.doubleCount }}</div>
</div>
</template>
store.js
const store = {
state: {
count: 1
},
getters: {
doubleCount(state, getter) {
console.log("getter", getter)
return state.count * 2
},
countAddOne(state) {
return state.count + 1
}
},
mutations: {
setCount(state, count) {
state.count = count
}
},
actions: {
setCount({ state, commit, dispatch }, count) { console.log("测试state", state); dispatch("testAction", "测试action"); setTimeout(() => { console.log("测试commit"); commit("setCount", count); }, 2000); }, testAction(context, payload) { console.log("in testActions", context, payload); }, }
}
export default store
- action有两个,在setCount中调用testAction
- setCount中输出state,提交commit
- getters有两个,输出参数getter
index.js
import Vue from "vue";
import Vuex from "./Vuex/index";
import Store from "./store";
import App from "./App.vue";
Vue.use(Vuex);
const store = new Vuex.Store(Store);
const app = new Vue({
store,
render: (h) => h(App),
}).$mount("#app");
Vuex/index.js
class Store {
static VueInstance = null;
}
function install(app, options = {}) {
Store.VueInstance = app;
}
export default { Store, install,};
解释一下Vuex/index.js 的雏形,Vue.use调用install方法,然后另外又导出了class Store,所以在入口文件中可以new Vuex.Store实例化
实现Vuex
准备好环境后,运行npm run dev,发现报错
很明显模板中用上了$store, 但是还未注入找不到就报错了,先注入一下,修改Vuex/index.js
class Store {
static VueInstance = null;
constructor(option) {
const { state = {}, getters = {}, mutations = {}, actions = {} } = option; // +++
const app = Store.VueInstance; // +++
app.mixin({ // +++
beforeCreate() {
if (this.$options.store) {
app.prototype.$store = this.$options.store;
}
},
});
}}
function install(app, options = {}) {
Store.VueInstance = app;
}
上述代码通过app.mixin向vue的原型注入$store,不知道app怎么来的同学请看上一篇文章
在入口文件把app输出一下,再次刷新界面,还是报错
此时虽然有了store,但是读取不到store,但是读取不到store,但是读取不到store.state.count 和 store.getters.count,先实现state吧,把store.getters.count,先实现state吧,把store.getters.count,先实现state吧,把store.getters.count那个div先注释
实现State
这里取上述第一种思路去实现state,要注意state不能直接修改,但是可以直接取值,所以这里使用代理模式去实现state
修改contrusctor,添加state的get 和 set方法
constructor(option) {
const { state = {}, getters = {}, mutations = {}, actions = {} } = option;
const app = Store.VueInstance;
this.vInstance = new app({ // +++
data: {
state
},
});
// ...
}
get state() { // +++
return this.vInstance;
}
set state(val) { // +++
console.error("can not set state directly");
}
- this.vInstance实现了一个小型vue,作为响应式的state
- 添加了state的set方法,直接设置state会报错
- 添加了state的get方法,代理this.vInstance
刷新浏览器可以看到
可以通过this.vInstance.state.count获得state中的count值,再次修改代码让state get方法指向this.vInstance.state
get state() {
return this.vInstance.state; // +++
}
刷新界面,state.count读取成功
在入口文件加上
store.state = 3;
直接修改state会报错,state的实现成功了,但是此时点击两个按钮都会报错,因为commit和dispatch方法还未实现
实现mutions和commit方法
思路
- 通过commit方法可以同步修改state中的某个值,commit有两个参数,第一个是对应在mutations中的方法名字,第二个是传给这个方法用的值payload
- 而mutations中的方法也有两个参数,第一个是state,第二个是commit时传进来的参数payload
实现
constructor(option) {
// ...
this.vInstance = new app({
data: {
state,
},
});
this.mutations = mutations // +++
this.commit = (muationName, payload) => { // +++
const method = mutations[muationName];
if (!method) {
console.error("no match mutation");
}
method.call(this, this.state, payload);
};
// ...
}
增加了实例的commit方法,增加了this.mutations
- 根据第一个参数找到对应在mutations中的方法,没找到报错
- 方法的this修正指向store实例,传入this.state和payload
刷新界面,测试一下commit方法
commit编写成功
实现actions和dispatch
mutations中的方法不支持异步操作,actions支持异步操作,而提交actions就需要dispatch方法
来看看官网对actions的介绍
思路
- dispatch有两个参数,第一个是对应actions中的方法名字,第二个是传给这个方法用的值payload
- 而actions中的方法也有两个参数,第一个参数是一个对象,具体看上图,第二个是dispatch时传进来的参数payload
实现
constructor(option) {
// ...
this.actions = actions;
this.dispatch = (actionName, payload) => {
const method = actions[actionName];
if (!method) {
console.error("no match mutation");
}
method.call(this,{
state: this.state,
commit: this.commit,
dispatch: this.dispatch,
},payload);
};
// ...
}
增加了实例的dispatch方法,增加了this.actions
- 根据第一个参数找到对应在actions中的方法,没找到报错
- 方法的this修正指向store实例,第一个参数是一个对象,拥有state,commit,dispatch是哪个参数(其实不止三个,这里暂时需要这三个够了)
- 传入payload
刷新界面,测试一下dispatch方法
点击action按钮2s后,count值变为2,并且能访问到state和触发其它action
实现getters
看看官网对getters的介绍
思路
- 初始化的时候需要遍历传入的getters中的所有key,访问store.getters其实是访问传入的getters,所以这里又是代理,代理中把this.state,this.getters传入作为参数
- getters依赖state,state发生变化则getters应该立即变化,state不变则getters不应该变化,是不是跟vue的计算属性有点像?
实现
constructor(option) {
const { state = {}, getters = {}, mutations = {}, actions = {} } = option;
// ...
this.getters = {}; // 添加实例的getters属性
Object.keys(getters).forEach((k) => { // 遍历option的getters
Object.defineProperty(this.getters, k, { // 添加代理,访问实例的getters相当于访问option的getters
get: () => {
return getters[k](this.state, this.getters); // 传入state和getters,使用的时候可以获取到
},
});
});
// ...
}
刷新界面
getters实现成功?
getters问题
下面改一下app.vue
<template>
<div>
<button @click="$store.commit('setCount', 4)">mutations</button>
<button @click="$store.dispatch('setCount', 2)">action</button>
<div>count: {{ $store.state.count }}</div>
<div>double1:{{ $store.getters.doubleCount }}</div>
<div>double2:{{ $store.getters.doubleCount }}</div>
<div>double3:{{ $store.getters.doubleCount }}</div>
</div>
</template>
刷新界面,getter输出了3次
想想computed的效果,当依赖的值没发生变化的时候,不会重新触发,所以getters还未实现成功
解决方案
文中已经提了多次computed,所以轮到它出场了,修改代码如下
constructor(option) {
const { state = {}, getters = {}, mutations = {}, actions = {} } = option;
// ... this.getters = {};
const computed = {}; // +++
Object.keys(getters).forEach((k) => {
computed[k] = () => getters[k](this.state, this.getters); // +++
Object.defineProperty(this.getters, k, {
get: () => {
return this.vInstance[k]; // +++
},
});
});
this.vInstance = new app({
data: {
state,
},
computed, // +++
});
}
- 添加一个对象computed,拥有getters中所有的方法并传入state和getters
- 对象computed同时也传入到this.vInstance中作为计算属性,因为vue的计算属性的效果就是依赖值不发生变化,就不会重新生成计算属性
- 代理指向改成 this.vInstance, 因为vue本身就把计算属性代理到vue实例上,此时访问this.vInstance就可以访问到传入的getters的所有属性
刷新浏览器
模板中多次调用同一个getters不会再重新生成,而是像computed一样缓存下来了
转载自:https://juejin.cn/post/7237392059929886777