辅助函数
Vuex 除了提供我们 Store 对象外,还对外提供了一系列的辅助函数,方便我们在代码中使用 Vuex,提供了操作 store 的各种属性的一系列语法糖,下面我们来一起看一下:
mapState
mapState 工具函数会将 store 中的 state 映射到局部计算属性中。为了更好理解它的实现,先来看一下它的使用示例:
// vuex 提供了独立的构建工具函数 Vuex.mapState import { mapState } from 'vuex' export default { // ... computed: mapState({ // 箭头函数可以让代码非常简洁 count: state => state.count, // 传入字符串 'count' 等同于 `state => state.count` countAlias: 'count', // 想访问局部状态,就必须借助于一个普通函数,函数中使用 `this` 获取局部状态 countPlusLocalState (state) { return state.count + this.localCount } }) }
当计算属性名称和状态子树名称对应相同时,我们可以向 mapState 工具函数传入一个字符串数组。
computed: mapState([ // 映射 this.count 到 this.$store.state.count 'count' ])
通过例子我们可以直观的看到,mapState 函数可以接受一个对象,也可以接收一个数组,那它底层到底干了什么事呢,我们一起来看一下源码这个函数的定义:
export function mapState (states) { const res = {} normalizeMap(states).forEach(({ key, val }) => { res[key] = function mappedState () { return typeof val === 'function' ? val.call(this, this.$store.state, this.$store.getters) : this.$store.state[val] } }) return res }
函数首先对传入的参数调用 normalizeMap 方法,我们来看一下这个函数的定义:
function normalizeMap (map) { return Array.isArray(map) ? map.map(key => ({ key, val: key })) : Object.keys(map).map(key => ({ key, val: map[key] })) }
这个方法判断参数 map 是否为数组,如果是数组,则调用数组的 map 方法,把数组的每个元素转换成一个 {key, val: key}
的对象;否则传入的 map 就是一个对象(从 mapState 的使用场景来看,传入的参数不是数组就是对象),我们调用 Object.keys 方法遍历这个 map 对象的 key,把数组的每个 key 都转换成一个 {key, val: key}
的对象。最后我们把这个对象数组作为 normalizeMap 的返回值。
回到 mapState 函数,在调用了 normalizeMap 函数后,把传入的 states 转换成由 {key, val} 对象构成的数组,接着调用 forEach 方法遍历这个数组,构造一个新的对象,这个新对象每个元素都返回一个新的函数 mappedState,函数对 val 的类型判断,如果 val 是一个函数,则直接调用这个 val 函数,把当前 store 上的 state 和 getters 作为参数,返回值作为 mappedState 的返回值;否则直接把 this.$store.state[val]
作为 mappedState 的返回值。
那么为何 mapState 函数的返回值是这样一个对象呢,因为 mapState 的作用是把全局的 state 和 getters 映射到当前组件的 computed 计算属性中,我们知道在 Vue 中 每个计算属性都是一个函数。
为了更加直观地说明,回到刚才的例子:
import { mapState } from 'vuex' export default { // ... computed: mapState({ // 箭头函数可以让代码非常简洁 count: state => state.count, // 传入字符串 'count' 等同于 `state => state.count` countAlias: 'count', // 想访问局部状态,就必须借助于一个普通函数,函数中使用 `this` 获取局部状态 countPlusLocalState (state) { return state.count + this.localCount } }) }
经过 mapState 函数调用后的结果,如下所示:
import { mapState } from 'vuex' export default { // ... computed: { count() { return this.$store.state.count }, countAlias() { return this.$store.state['count'] }, countPlusLocalState() { return this.$store.state.count + this.localCount } } }
我们再看一下 mapState 参数为数组的例子:
computed: mapState([ // 映射 this.count 到 this.$store.state.count 'count' ])
经过 mapState 函数调用后的结果,如下所示:
computed: { count() { return this.$store.state['count'] } }
mapGetters
mapGetters 工具函数会将 store 中的 getter 映射到局部计算属性中。它的功能和 mapState 非常类似,我们来直接看它的实现:
export function mapGetters (getters) { const res = {} normalizeMap(getters).forEach(({ key, val }) => { res[key] = function mappedGetter () { if (!(val in this.$store.getters)) { console.error(`[vuex] unknown getter: ${val}`) } return this.$store.getters[val] } }) return res }
mapGetters 的实现也和 mapState 很类似,不同的是它的 val 不能是函数,只能是一个字符串,而且会检查 val in this.$store.getters
的值,如果为 false 会输出一条错误日志。为了更直观地理解,我们来看一个简单的例子:
import { mapGetters } from 'vuex' export default { // ... computed: { // 使用对象扩展操作符把 getter 混入到 computed 中 ...mapGetters([ 'doneTodosCount', 'anotherGetter', // ... ]) } }
经过 mapGetters 函数调用后的结果,如下所示:
import { mapGetters } from 'vuex' export default { // ... computed: { doneTodosCount() { return this.$store.getters['doneTodosCount'] }, anotherGetter() { return this.$store.getters['anotherGetter'] } } }
再看一个参数 mapGetters 参数是对象的例子:
computed: mapGetters({ // 映射 this.doneCount 到 store.getters.doneTodosCount doneCount: 'doneTodosCount' })
经过 mapGetters 函数调用后的结果,如下所示:
computed: { doneCount() { return this.$store.getters['doneTodosCount'] } }
mapActions
mapActions 工具函数会将 store 中的 dispatch 方法映射到组件的 methods 中。和 mapState、mapGetters 也类似,只不过它映射的地方不是计算属性,而是组件的 methods 对象上。我们来直接看它的实现:
export function mapActions (actions) { const res = {} normalizeMap(actions).forEach(({ key, val }) => { res[key] = function mappedAction (...args) { return this.$store.dispatch.apply(this.$store, [val].concat(args)) } }) return res }
可以看到,函数的实现套路和 mapState、mapGetters 差不多,甚至更简单一些, 实际上就是做了一层函数包装。为了更直观地理解,我们来看一个简单的例子:
import { mapActions } from 'vuex' export default { // ... methods: { ...mapActions([ 'increment' // 映射 this.increment() 到 this.$store.dispatch('increment') ]), ...mapActions({ add: 'increment' // 映射 this.add() to this.$store.dispatch('increment') }) } }
经过 mapActions 函数调用后的结果,如下所示:
import { mapActions } from 'vuex' export default { // ... methods: { increment(...args) { return this.$store.dispatch.apply(this.$store, ['increment'].concat(args)) } add(...args) { return this.$store.dispatch.apply(this.$store, ['increment'].concat(args)) } } }
mapMutations
mapMutations 工具函数会将 store 中的 commit 方法映射到组件的 methods 中。和 mapActions 的功能几乎一样,我们来直接看它的实现:
export function mapMutations (mutations) { const res = {} normalizeMap(mutations).forEach(({ key, val }) => { res[key] = function mappedMutation (...args) { return this.$store.commit.apply(this.$store, [val].concat(args)) } }) return res }
函数的实现几乎也和 mapActions 一样,唯一差别就是映射的是 store 的 commit 方法。为了更直观地理解,我们来看一个简单的例子:
import { mapMutations } from 'vuex' export default { // ... methods: { ...mapMutations([ 'increment' // 映射 this.increment() 到 this.$store.commit('increment') ]), ...mapMutations({ add: 'increment' // 映射 this.add() 到 this.$store.commit('increment') }) } }
经过 mapMutations 函数调用后的结果,如下所示:
import { mapActions } from 'vuex' export default { // ... methods: { increment(...args) { return this.$store.commit.apply(this.$store, ['increment'].concat(args)) } add(...args) { return this.$store.commit.apply(this.$store, ['increment'].concat(args)) } } }
插件
Vuex 的 store 接收 plugins 选项,一个 Vuex 的插件就是一个简单的方法,接收 store 作为唯一参数。插件作用通常是用来监听每次 mutation 的变化,来做一些事情。
在 store 的构造函数的最后,我们通过如下代码调用插件:
import devtoolPlugin from './plugins/devtool' // apply plugins plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
我们通常实例化 store 的时候,还会调用 logger 插件,代码如下:
import Vue from 'vue' import Vuex from 'vuex' import createLogger from 'vuex/dist/logger' Vue.use(Vuex) const debug = process.env.NODE_ENV !== 'production' export default new Vuex.Store({ ... plugins: debug ? [createLogger()] : [] })
在上述 2 个例子中,我们分别调用了 devtoolPlugin 和 createLogger() 2 个插件,它们是 Vuex 内置插件,我们接下来分别看一下他们的实现。
devtoolPlugin
devtoolPlugin 主要功能是利用 Vue 的开发者工具和 Vuex 做配合,通过开发者工具的面板展示 Vuex 的状态。它的源码在 src/plugins/devtool.js 中,来看一下这个插件到底做了哪些事情。
const devtoolHook = typeof window !== 'undefined' && window.__VUE_DEVTOOLS_GLOBAL_HOOK__ export default function devtoolPlugin (store) { if (!devtoolHook) return store._devtoolHook = devtoolHook devtoolHook.emit('vuex:init', store) devtoolHook.on('vuex:travel-to-state', targetState => { store.replaceState(targetState) }) store.subscribe((mutation, state) => { devtoolHook.emit('vuex:mutation', mutation, state) }) }
我们直接从对外暴露的 devtoolPlugin 函数看起,函数首先判断了devtoolHook 的值,如果我们浏览器装了 Vue 开发者工具,那么在 window 上就会有一个 __VUE_DEVTOOLS_GLOBAL_HOOK__
的引用, 那么这个 devtoolHook 就指向这个引用。
接下来通过 devtoolHook.emit('vuex:init', store)
派发一个 Vuex 初始化的事件,这样开发者工具就能拿到当前这个 store 实例。
接下来通过 devtoolHook.on('vuex:travel-to-state', targetState => { store.replaceState(targetState) })
监听 Vuex 的 traval-to-state 的事件,把当前的状态树替换成目标状态树,这个功能也是利用 Vue 开发者工具替换 Vuex 的状态。
最后通过 store.subscribe((mutation, state) => { devtoolHook.emit('vuex:mutation', mutation, state) })
方法订阅 store 的 state 的变化,当 store 的 mutation 提交了 state 的变化, 会触发回调函数——通过 devtoolHook 派发一个 Vuex mutation 的事件,mutation 和 rootState 作为参数,这样开发者工具就可以观测到 Vuex state 的实时变化,在面板上展示最新的状态树。
loggerPlugin
通常在开发环境中,我们希望实时把 mutation 的动作以及 store 的 state 的变化实时输出,那么我们可以用 loggerPlugin 帮我们做这个事情。它的源码在 src/plugins/logger.js 中,来看一下这个插件到底做了哪些事情。
// Credits: borrowed code from fcomb/redux-logger import { deepCopy } from '../util' export default function createLogger ({ collapsed = true, transformer = state => state, mutationTransformer = mut => mut } = {}) { return store => { let prevState = deepCopy(store.state) store.subscribe((mutation, state) => { if (typeof console === 'undefined') { return } const nextState = deepCopy(state) const time = new Date() const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}` const formattedMutation = mutationTransformer(mutation) const message = `mutation ${mutation.type}${formattedTime}` const startMessage = collapsed ? console.groupCollapsed : console.group // render try { startMessage.call(console, message) } catch (e) { console.log(message) } console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState)) console.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation) console.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState)) try { console.groupEnd() } catch (e) { console.log('—— log end ——') } prevState = nextState }) } } function repeat (str, times) { return (new Array(times + 1)).join(str) } function pad (num, maxLength) { return repeat('0', maxLength - num.toString().length) + num }
插件对外暴露的是 createLogger 方法,它实际上接受 3 个参数,它们都有默认值,通常我们用默认值就可以。createLogger 的返回的是一个函数,当我执行 logger 插件的时候,实际上执行的是这个函数,下面来看一下这个函数做了哪些事情。
函数首先执行了 let prevState = deepCopy(store.state)
深拷贝当前 store 的 rootState。这里为什么要深拷贝,因为如果是单纯的引用,那么 store.state 的任何变化都会影响这个引用,这样就无法记录上一个状态了。我们来了解一下 deepCopy 的实现,在 src/util.js 里定义:
function find (list, f) { return list.filter(f)[0] } export function deepCopy (obj, cache = []) { // just return if obj is immutable value if (obj === null || typeof obj !== 'object') { return obj } // if obj is hit, it is in circular structure const hit = find(cache, c => c.original === obj) if (hit) { return hit.copy } const copy = Array.isArray(obj) ? [] : {} // put the copy into cache at first // because we want to refer it in recursive deepCopy cache.push({ original: obj, copy }) Object.keys(obj).forEach(key => { copy[key] = deepCopy(obj[key], cache) }) return copy }
deepCopy 并不陌生,很多开源库如 loadash、jQuery 都有类似的实现,原理也不难理解,主要是构造一个新的对象,遍历原对象或者数组,递归调用 deepCopy。不过这里的实现有一个有意思的地方,在每次执行 deepCopy 的时候,会用 cache 数组缓存当前嵌套的对象,以及执行 deepCopy 返回的 copy。如果在 deepCopy 的过程中通过 find(cache, c => c.original === obj)
发现有循环引用的时候,直接返回 cache 中对应的 copy,这样就避免了无限循环的情况。
回到 loggerPlugin 函数,通过 deepCopy 拷贝了当前 state 的副本并用 prevState 变量保存,接下来调用 store.subscribe 方法订阅 store 的 state 的变。 在回调函数中,也是先通过 deepCopy 方法拿到当前的 state 的副本,并用 nextState 变量保存。接下来获取当前格式化时间已经格式化的 mutation 变化的字符串,然后利用 console.group 以及 console.log 分组输出 prevState、mutation以及 nextState,这里可以通过我们 createLogger 的参数 collapsed、transformer 以及 mutationTransformer 来控制我们最终 log 的显示效果。在函数的最后,我们把 nextState 赋值给 prevState,便于下一次 mutation。
作者: ustbhuangyi
链接:http://www.imooc.com/article/14741
来源:慕课网
本文原创发布于慕课网 ,转载请注明出处,谢谢合作