Vuex
项目创建
上一篇 手写简易Vue-router 里我们手写了一个简易的vue-router,这次来挑战一下实现vuex基本功能。老样子,我们先创建一个项目,使用一下 Vuex
相关组件和vuex配置
main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App)
}).$mount('#app')
App.vue
<template>
<div id="app">
<div class="hello">
<p @click="$store.commit('add')">counter: {{$store.state.counter}}</p>
<p @click="$store.dispatch('add')">async: {{$store.state.counter}}</p>
</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
}
</script>
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
然后是 vuex 配置 store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
counter: 0
},
mutations: {
add(state) {
state.counter++
}
},
actions: {
add({commit}) {
setTimeout(() => {
commit('add')
}, 1000);
}
}
})
项目启动
这样就成功实现了在组件中使用vuex
下面我们的目标就是编码实现自己的vuex
手写 Vuex
文件准备
创建mystore文件
main.js
import Vue from 'vue'
import App from './App.vue'
// import store from './store'
import store from './mystore' //store指向改变
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App)
}).$mount('#app')
mystore/index.js
import Vue from "vue";
import Vuex from "./myvuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
counter: 0,
},
mutations: {
add(state) {
state.counter++;
},
},
actions: {
add({ commit }) {
setTimeout(() => {
commit("add");
}, 1000);
},
}
});
需求分析
和实现vue-router一样,要想实现vuex,有以下一些需求需要实现:
- vuex作为一个插件使用,需要实现store类和install方法
- sotre类的实现要点:
- 需要有响应式的数据 state
- store实例需要挂载到vue实例上
- commit 和 dispatch 方法实现
vuex 基本结构
回想vuex使用时的步骤
- 先引入
import Vuex from 'vuex' - 再安装
Vue.use(Vuex); - 最后实例化
new Vuex.Store({...})
和vue-router不同,实例化的并不是 vuex,而是 Vuex.Store。这说明什么,说明Vuex本身就包含了 store构造函数 和 install方法,也就是说vuex应当有着如下的基本结构
mystore/myvuex.js
let Vue
class Store{
constructor(){
}
}
function install(_Vue) {
}
export default {Store,install}
vue组件上挂载 $store 实例
和vue-router 一样 vue组件上添加store实例可以通过混入的办法来实现。(因为 Vue.use(Vuex) 执行在先,install方法中不能直接拿到 store实例。)、
//...class Store
function install(_Vue) {
Vue=_Vue
console.log(Vue)
Vue.mixin({
beforeCreate() {
// 此时,上下文已经是组件实例了
// 如果this是根实例,则它的$options里面会有store实例
if (this.$options.store) {
Vue.prototype.$store = this.$options.store;
//以后就能在组件中拿到 $store
}
},
});
}
响应式数据 state
需要在store类中定义响应式数据state, 组件中便能使用 $store.state, 当state发生改变时页面也能更新
let Vue
class Store{
constructor(options){
//保存选项
this.$options=options;
const state=this.$options.state || {};
//定义响应式数据$$state
Vue.util.defineReactive(this, "$$state", state);
}
//对外暴露state
get state(){
return this.$$state
}
set state(v){
console.error('please use replaceState to reset state');
}
}
实现commit 和 dispatch 方法
这里贴上全部代码
let Vue
class Store{
constructor(options){
// 保存mutations 和 actions
this._mutations = options.mutations
this._actions = options.actions
this.$options=options;
const state=this.$options.state || {};
Vue.util.defineReactive(this, "$$state", state);
//为避免this的指向出现问题,可以将其绑定到当前store实例
this.commit = this.commit.bind(this)
this.dispatch=this.dispatch.bind(this)
}
commit(type,payload){
//根据type找到用户定义的mutation方法
const entry = this._mutations[type];
if(entry){
entry(this.state,payload)
}
}
dispatch(type,payload){
const entry = this._actions[type];
console.log(`entry`, entry);
if (entry) {
entry(this, payload);
}
}
get state(){
return this.$$state
}
set state(v){
console.error('please use replaceState to reset state');
}
}
function install(_Vue) {
Vue=_Vue
Vue.mixin({
beforeCreate() {
// 此时,上下文已经是组件实例了
// 如果this是根实例,则它的$options里面会有store实例
if (this.$options.store) {
Vue.prototype.$store = this.$options.store;
}
},
});
}
export default { Store, install };
大功告成,vuex的基本功能就实现了,是不是很简单,大家可以一起动手试一试
实现getters
现在我们更近一步,来尝试实现getters
app.vue
<template>
<div id="app">
<div class="hello">
<p @click="$store.commit('add')">counter: {{$store.state.counter}}</p>
<p @click="$store.dispatch('add')">async: {{$store.state.counter}}</p>
<p >doubleCounter: {{$store.getters.doubleCounter}}</p>
</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
}
</script>
//...
mystore/index.js
import Vue from "vue";
import Vuex from "./myvuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
counter: 0,
},
//..mutation,action 配置
getters: {
doubleCounter(state) {
return state.counter * 2;
},
},
});
数据代理
我们知道getters 用于获取 state 里的数据,如果要获取的数据并没有发生变化的话,就会返回缓存的数据。
等一下,缓存数据?有没有想到什么,是不是很像计算属性,那么我们能不能通过vue的实例来代理数据呢,做法如下:
mystore/myvuex.js
let Vue
class Store{
constructor(options){
// 保存mutations 和 actions
this.$options=options;
const state=this.$options.state || {};
// Vue.util.defineReactive(this, "$$state", state);
// 新建vue来代理数据
this._vm=new Vue({
data:{
$$state:state
}
})
//绑定commit和dispatch的指向
}
//commit 和 dispatch实现
get state(){
// return this.$$state
// 通过vue代理state
return this._vm._data.$$state
}
set state(v){
console.error('please use replaceState to reset state');
}
}
通过计算属性代理getters
我们知道 computed中的函数是无参数的,而getters在计算时候需要出入state,所以还要做高一级的函数封装
let Vue
class Store{
constructor(options){
// 保存mutations 和 actions
this.$options=options;
const state=this.$options.state || {};
//保存传入的getters配置
this._wrapedGetters=options.getters;
this.getters={}
//定义computed对象
const computed={}
//避免this指向出错
const store=this;
//本例中 ['doubleCounter'] key=> doubleCounter
Object.keys(this._wrapedGetters).forEach((key) => {
const fn = this._wrapedGetters[key];
//computed:{
// doubleCounter:function(){ return doubleCounter(store.state)}
//}
//再用一层函数包裹封装
computed[key] = function(){
return fn(store.state)
}
//外界访问 $store.getters.doubleCounter, 指向 store._vm 中对应代理的computed属性
Object.defineProperty(store.getters,key,{
get:function(){
return store._vm[key]
}
})
});
this._vm=new Vue({
data:{
$$state:state
},
computed
})
//绑定commit和dispatch的指向
}
//...
}
全部myVuex代码
let Vue
class Store{
constructor(options){
// 保存mutations 和 actions
this._mutations = options.mutations
this._actions = options.actions
this.$options=options;
const state=this.$options.state || {};
this._wrapedGetters=options.getters;
this.getters={}
const computed={}
const store=this;
Object.keys(this._wrapedGetters).forEach((key) => {
const fn = this._wrapedGetters[key];
computed[key] = function(){
return fn(store.state)
}
Object.defineProperty(store.getters,key,{
get:function(){
return store._vm[key]
}
})
});
this._vm=new Vue({
data:{
$$state:state
},
computed
})
// Vue.util.defineReactive(this, "$$state", state);
this.commit = this.commit.bind(this)
this.dispatch=this.dispatch.bind(this)
}
commit(type,payload){
console.log(this)
const entry = this._mutations[type];
if(entry){
entry(this.state,payload)
}
}
dispatch(type,payload){
const entry = this._actions[type];
console.log(`entry`, entry);
if (entry) {
entry(this, payload);
}
}
get state(){
// return this.$$state
return this._vm._data.$$state
}
set state(v){
console.error('please use replaceState to reset state');
}
}
function install(_Vue) {
Vue=_Vue
Vue.mixin({
beforeCreate() {
// 此时,上下文已经是组件实例了
// 如果this是根实例,则它的$options里面会有store实例
if (this.$options.store) {
Vue.prototype.$store = this.$options.store;
}
},
});
}
export default { Store, install };