Vuex实现TodoList及部分源码分析
1.安装Vuex
npm install vuex --save
2.在src
下创建store
文件夹,再分别创建index.js
actions.js
mutations.js
文件
- 在
index.js
中引入vue
,vuex
- 再引入
actions.js
,mutations.js
文件 - 导出默认出口
// index.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import actions from './actions'
//使用vuex
Vue.use(Vuex)
//创建vuex实例
export default new Vuex.Store({
state: {
todos: []
},
actions,
mutations,
})
3.在main.js
中引入文件,在vue
实例全局引入store
实例
// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
store, //把store对象注入所有的子组件
render: h => h(App),
}).$mount('#app')
TodoList实现
state
Vuex
使用单一状态树,每个应用仅包含一个store
实例。其中state
包含全部的应用状态,因此将todos
存储在state
中,通过this.$store.state.todos
访问
mutation
要想更改store
中状态的唯一方法是提交mutation
,每个mutation
都有一个字符串的事件类型 (type) 和 一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受state
作为第一个参数,需要对状态进行改变时可传入第二个参数:
export const mutations = {
addTodo (state, todo) {
state.todos.push(todo);
},
removeTodo (state, todo) {
state.todos.splice(state.todos.indexOf(todo), 1)
},
editTodo (state, { todo, text = todo.text, done = todo.done }) {
todo.text = text
todo.done = done
}
}
通过store.commit
触发,或者使用mapMutations
辅助函数将组件中的methods
映射为store.commit
调用(需要在根节点注入store
):
this.$store.commit("addTodo", todo);
this.$store.commit("removeTodo", todo);
this.$store.commit("editTodo", {todo, text:value, done:!todo.done});
action
action
类似于mutation
,区别是:
-
mutation
是修改state
的唯一途径,action
提交mutation
,但不能直接修改state
-
mutation
必须同步执行,action
可以进行异步操作
注册简单的action
:
export default {
addTodo ({ commit }, text) {
commit('addTodo', {
text,
done: false
})
},
removeTodo ({ commit }, todo) {
commit('removeTodo', todo)
},
changeTodo ({ commit }, todo) {
commit('editTodo', { todo, done: !todo.done })
},
editTodo ({ commit }, { todo, value }) {
commit('editTodo', { todo, text: value })
},
}
action
通过store.dispatch
触发,或者使用mapActions
辅助函数将组件的methods
映射为store.dispatch
调用(需要先在根节点注入store
):
methods: {
...mapActions([
'editTodo',
'changeTodo',
'removeTodo'
]),
doneEdit (e) {
const value = e.target.value.trim()
const { todo } = this
if (!value) {
this.removeTodo(todo)
} else if (this.editing) {
this.editTodo({
todo,
value
})
this.editing = false
}
},
······
}
Vuex工作流可参考如下图片:
Vuex流程图Vuex官方文档: Vuex 是什么?
辅助函数源码分析
为了避免每次都需要通过this.$store
来调用api,vuex
提供了mapState
mapMutations
mapGetters
mapActions
createNamespaceHelpers
等api。具体实现存放在src/helper.js
中:
-
一些工具函数
下面这些函数都是实现以上提到的 api 所用到的:
/**
* 统一数据格式,将数组或对象展现成如下格式
* normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
* normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
* @param {Array|Object} map
* @return {Object}
*/
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
// Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组
}
/**
* 返回一个函数,参数分别为namespace和map,判断是否存在namespace,统一进行namespace处理
* @param {Function} fn
* @return {Function}
*/
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
// charAt() 方法可返回指定位置的字符
namespace += '/'
}
return fn(namespace, map)
}
}
/**
* 通过namespace获取module
* @param {Object} store
* @param {String} helper
* @param {String} namespace
* @return {Object}
*/
function getModuleByNamespace (store, helper, namespace) {
// _modulesNamespaceMap参考src/store.js
const module = store._modulesNamespaceMap[namespace]
if (process.env.NODE_ENV !== 'production' && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}
-
mapState
为组件创建计算属性返回store
中的状态:
/**
* 减少在vue.js中获取state的代码
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} states # 对象的项可以是一个接收state和getter的参数的函数,你可以在其中对state和getter做一些事情。
* @return {Object}
*/
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
// 统一数组格式并遍历数组
normalizeMap(states).forEach(({ key, val }) => {
// 返回一个对象,值都是函数
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
// 获取module
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
// 获取module的state和getters
state = module.context.state
getters = module.context.getters
}
// Object类型的val是函数,传递过去的参数是state和getters
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
mapState
是normalizeNamespace
的返回值。首先利用normalizeMap
对states
进行格式的统一,然后遍历,对参数的所有state
包裹一层函数,返回一个对象。
-
mapGetters
/**
* 减少获取getters的代码
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} getters
* @return {Object}
*/
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
normalizeMap(getters).forEach(({ key, val }) => {
// The namespace has been mutated by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
同样的处理方式,遍历getters
,只是这里需要加上命名空间,这是因为在注册时_wrapGetters
中的getters
是有加上命名空间的
-
mapMutations
创建组件方法提交mutation
/**
* 减少提交mutation的代码
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} mutations # 对象的项可以是一个接受'commit '函数作为第一个参数的函数,它还可以接受另一个参数。你可以在这个函数中提交变异和做任何其他事情。特别是,您需要从映射函数传递另一个参数
* @return {Object}
*/
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
normalizeMap(mutations).forEach(({ key, val }) => {
// 返回一个对象,值是函数
res[key] = function mappedMutation (...args) {
// Get the commit method from store
let commit = this.$store.commit
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
if (!module) {
return
}
commit = module.context.commit
}
// 执行mutation
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
}
})
return res
})
判断是否存在namespace
后,commit
是不一样的,每个module
都是保存了上下文的,这里如果存在namespace
就需要使用那个另外处理的commit
等信息
-
mapActions
/**
* 减少派发action 的代码
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} actions # 对象的项可以是一个接受“派发”函数作为第一个参数的函数,它还可以接受另一个参数。可以在这个函数中调度action并执行其他任何操作。
* @return {Object}
*/
export const mapActions = normalizeNamespace((namespace, actions) => {
const res = {}
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
// get dispatch function from store
let dispatch = this.$store.dispatch
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
if (!module) {
return
}
dispatch = module.context.dispatch
}
return typeof val === 'function'
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args))
}
})
return res
})
与mapMutations
的处理方式相似
辅助函数的主要目的是减少代码量,通过各类api直接在文件中调用函数改变状态,不需要通过this.$store
一步步进行操作。