这个月会实现一下Vue
, Vuex
, vue-router
。我会以倒推 的模式边开发边写文章。话不多说开始跟着我一起撸。仓库地址
本文只是实现了一个非常基础版本的vuex
,日后会进行升级。且本文所写的代码,不会每个地方都做异常判断,只满足我表达vuex
的实现原理即可。
回忆下我们平时代码里用到vuex
必定会有的两行代码
1 2 3 Vue.use(Vuex); store = new Vuex.Store({});
所以vuex
需要导出两个方法,一个提供use
的install
方法,一个就是Store
1 2 3 4 5 6 export default { install, Store, };
诸如vuex
、vue-router
等都是使用Vue.mixin
,往每个组件上挂载beforeCreate
生命周期来初始化。
上代码
1 2 3 4 5 6 7 8 9 10 11 12 const install = (_Vue ) => { Vue = _Vue; Vue.mixin({ beforeCreate() { if (this .$options.store) { this .$store = this .$options.store; } else { this .$store = this .$parent && this .$parent.$store; } }, }); };
Vue
的use
方法我就不解释了,感兴趣的可以去看下vue
源码,他是调用use
参数的install
方法,并传入Vue
构造函数。 那么上述为什么会判断this.$options.store
和读取$parent.$store
呢?
这个时候请回忆下我们new Vue
的都会传入store
,所以当this.$options.store
获取不到,代表他不是根节点(当然子组件也可以加store
,这里只是常规分析)。this.$store = this.$parent && this.$parent.$store;
,那么他很有可能就是子组件,就会向上层获取,上层肯定会有。就这样一层层往下传递。
install
讲完了,接下来就讲我们的重头戏,Store
;
Store 下面我将按照以下几个属性进行讲解
state
mapState
getters
mapGetters
mutations
mapMutations
action
mapActions
state Store
中的响应式都是借助Vue
来进行数据响应式。
我先在Vuex
注册的时候,加个state
属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 import Vuex from './vuex/index.js' ;import Vue from 'vue' ;Vue.use(Vuex); const store = new Vuex.Store({ state: { count: 1 , }, }); export default store;
这个时候,源码上就要支持state
的获取
所以获取 state 的时候,直接读取vm.state
,且state
只能获取不能修改
1 2 3 get state() { return this .vm.state; }
得到代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 let Vue;const install = (_Vue ) => { }; class Store { constructor (options) { console .log(options, 'options' ); this .vm = new Vue({ data: { state: options.state, }, }); } get state() { return this .vm.state; } } export default { install, Store, };
编写vue
组件代码
1 2 3 4 5 6 7 8 9 10 11 12 13 <template > <div id ="app" > <div > {{ $store.state.a }}{{ $store.state.b }}</div > </div > </template > <script > export default { name: 'App' , created() { console .log(this .$store.state, 'store' ); }, }; </script >
打开页面看下效果,页面成功读取到state
中的值了;
mapState 我们一般使用mapState
会有如下几种用法
1 2 3 4 5 6 7 8 9 export default { computed: { ...mapState(['count' ]), ...mapState({ count2: 'count' , count3: (state ) => `${state.count} xixi` , }), }, };
我们可以看到,mapState
支持数组、对象、对象中的value
还可以以函数的形式存在。 所以我们实现的时候
如果是数组,就直接从state
中取每个值。
如果是对象,就将对象的每个键的值赋值为一个函数,并传入 state 这个对象。
实现方式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function normalizeMap (map ) { return Array .isArray(map) ? map.map((key ) => ({ key, val : key })) : Object .keys(map).map((key ) => ({ key, val : map[key] })); } export const mapState = (options ) => { if (typeof options !== 'object' ) { console .error( '[vuex] mapActions: mapper parameter must be either an Array or an Object' ); return ; } const res = {}; normalizeMap(options).forEach((o ) => { if (typeof o.val === 'function' ) { res[o.key] = function ( ) { return o.val.call(this .$store, this .$store.state); }; } else { res[o.key] = function ( ) { return this .$store.state[o.val]; }; } }); return res; };
getters getters
其实非常的简单,就是调用他的方法,传入state
参数
1 <div > getsname: {{ $store.getters.getsname }}</div >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Store { constructor (options) { console .log(options, 'options' ); + this .getters = options.getters; this .vm = new Vue({ data: { state: options.state } }); + Object .keys(this .getters).forEach(key => { + this .getters[key] = this .getters[key].call(this , this .vm.state); + }); + } get state() { return this .vm.state; } }
mapGetters mapGetters
其实与mapState
非常的相似,只是传参发生了变化。变成了(state, getters)
,我就不过多解释了。
1 2 3 <div > getsname: {{ getsname }}</div > <div > getsname2: {{ getsname2 }}</div > <div > getsname3: {{ getsname3 }}</div >
1 2 3 4 5 6 7 8 9 10 11 export default { computed: { ...mapGetters(['getsname' ]), ...mapGetters({ getsname2: 'getsname' , getsname3: (state, getters ) => `count: ${state.count} , getsname: ${getters.getsname} ` , }), }, };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export const mapGetters = (options ) => { if (typeof options !== 'object' ) { console .error( '[vuex] mapActions: mapper parameter must be either an Array or an Object' ); return ; } const res = {}; normalizeMap(options).forEach((o ) => { if (typeof o.val === 'function' ) { res[o.key] = function ( ) { return o.val.call(this .$store, this .$store.state, this .$store.getters); }; } else { res[o.key] = function ( ) { return this .$store.getters[o.val]; }; } }); return res; };
mutations mutations
其实也是非常的简单,只需要写个commit
方法,并触发下mutations
里对应的方法,并传入state
跟用户的参数两个变量就行了。
1 2 3 <button @click ="$store.commit('increment', 1)" > mutation:increment => {{ count }} </button >
1 2 3 4 5 6 class Store { commit(event, payload) { this .mutations[event].call(this , this .state, payload); } }
mapMutations mapMutations
与上面的mapState, mapGetters
差不多,就是遍历mutations
返回一个对象。
1 2 <button @click ="increment(1)" > mapMutations mutation:increment => {{ count }}</button > <button @click ="increment2(2)" > increment2 mutation:increment => {{ count }}</button >
1 2 3 4 5 6 7 8 export default { methods: { ...mapMutations(['increment' ]), ...mapMutations({ increment2: 'increment' }) } }
1 2 3 4 5 6 7 8 9 10 11 12 13 export const mapMutations = options => { if (typeof options !== 'object' ) { console .log('本实例暂不支持module方式' ); return {}; } const res = {}; normalizeMap(options).forEach(o => { res[o.key] = function (option ) { return this .$store.commit(o.val, option); }; }); return res; };
normalizeMap
在上述map
方法中使用到了,主要用途就是将数组或者对象处理成一个包含key, value
对象。
actions Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作;
1 2 3 4 5 asyncIncrement(context, payload) { setTimeout(() => { context.commit('increment' , payload.count); }, 5000 ); }
实现其实也是非常简单的,就是触发下注册的action
而已。
先上代码
1 2 3 4 5 6 7 8 dispatch(event, payload) { if (isObject(event)) { const { type, ...res } = event; event = type; payload = res; } this .actions[event].call(this , this , payload); }
这里加了层判断第一个参数是否是对象,如果是对象,事件名就取对象中的type
字段,payload
就取剩下的。
三种基本操作(常规操作、参数放在一个对象中传、异步操作)例子如下
1 2 3 <button @click ="$store.dispatch('actionIncrement', { count: 3 })" > test dispatch ===> actionIncrement</button > <button @click ="$store.dispatch({ type: 'actionIncrement', count: 4 })" > test dispatch ===> actionIncrement</button > <button @click ="$store.dispatch({ type: 'asyncIncrement', count: 4 })" > test dispatch ===> asyncIncrement</button >
1 2 3 4 5 6 7 8 9 10 11 12 13 const store = new Vuex.Store({ actions: { actionIncrement(context, payload) { context.commit('increment' , payload.count); }, asyncIncrement(context, payload) { setTimeout(() => { context.commit('increment' , payload.count); }, 5000 ); } } });
mapActions mapActions
与mapMutations
基本逻辑是一致的,一个是用commit
触发mutations
中的方法,一个是用dispatch
触发actions
中的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 export const mapActions = options => { if (typeof options !== 'object' ) { console .log('本实例暂不支持module' ); return {}; } const res = {}; normalizeMap(options).forEach(o => { res[o.key] = function (option ) { return this .$store.dispatch(o.val, option); }; }); return res; };
vue
页面例子如下
1 2 3 4 5 6 <div > <p > 测试 mapActions</p > <button @click ="actionIncrement({count: 5})" > 常规操作actionIncrement</button > <button @click ="asyncIncrement({count: 6})" > 异步操作asyncIncrement</button > <button @click ="actionIncrement3({count: 7})" > 更换名字actionIncrement3</button > </div >
1 2 3 4 5 6 7 8 export default { methods: { ...mapActions(['actionIncrement' , 'asyncIncrement' ]), ...mapActions({ actionIncrement3: 'actionIncrement' }) } }
到此vuex
基本功能已经全部满足了。日后有心思的话,会对文章及代码进行升级,并实现一套完整的vuex
。
讲解代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 let Vue;const install = _Vue => { if (Vue) { return console .log('请勿重复安装' ); } Vue = _Vue; _Vue.mixin({ beforeCreate() { if (this .$options.store) { this .$store = this .$options.store; } else { this .$store = this .$parent && this .$parent.$store; } } }); }; function normalizeMap (map ) { return Array .isArray(map) ? map.map(key => ({ key, val : key })) : Object .keys(map).map(key => ({ key, val : map[key] })); } function isObject (obj ) { return Object .prototype.toString.call(obj).slice(8 , -1 ) === 'Object' ; } class Store { constructor (options) { this .getters = options.getters || {}; this .mutations = options.mutations || {}; this .actions = options.actions || {}; this .vm = new Vue({ data: { state: options.state } }); Object .keys(this .getters).forEach(key => { this .getters[key] = this .getters[key].call(this , this .vm.state); }); } get state() { return this .vm.state; } commit(event, payload) { this .mutations[event].call(this , this .state, payload); } dispatch(event, payload) { console .log(event, 'event' ) if (isObject(event)) { const { type, ...res } = event; event = type; payload = res; } this .actions[event].call(this , this , payload); } } export const mapState = options => { if (typeof options !== 'object' ) { console .error('[vuex] mapActions: mapper parameter must be either an Array or an Object' ); return ; } const res = {}; normalizeMap(options).forEach(o => { if (typeof o.val === 'function' ) { res[o.key] = function ( ) { return o.val.call(this .$store, this .$store.state); }; } else { res[o.key] = function ( ) { return this .$store.state[o.val]; }; } }); return res; }; export const mapGetters = options => { if (typeof options !== 'object' ) { console .error('[vuex] mapActions: mapper parameter must be either an Array or an Object' ); return ; } const res = {}; normalizeMap(options).forEach(o => { if (typeof o.val === 'function' ) { res[o.key] = function ( ) { return o.val.call(this .$store, this .$store.state, this .$store.getters); }; } else { res[o.key] = function ( ) { console .log(this .$store.state, o.value, '$sotre' ); return this .$store.getters[o.val]; }; } }); return res; }; export const mapMutations = options => { if (typeof options !== 'object' ) { console .log('本实例暂不支持module' ); return {}; } const res = {}; normalizeMap(options).forEach(o => { res[o.key] = function (option ) { return this .$store.commit(o.val, option); }; }); return res; }; export const mapActions = options => { if (typeof options !== 'object' ) { console .log('本实例暂不支持module' ); return {}; } const res = {}; normalizeMap(options).forEach(o => { res[o.key] = function (option ) { return this .$store.dispatch(o.val, option); }; }); return res; }; export default { install, Store };