auxiliary function
In addition to providing our Store object, Vuex also provides a series of auxiliary functions to facilitate the use of Vuex in our code, and provides a series of grammatical sugar to manipulate various attributes of the store. Let's take a look at the following:
mapState
The mapState tool function maps the state in the store to locally computed properties. To better understand its implementation, let's first look at an example of its use:
// vuex provides a separate build tool function, Vuex.mapState import { mapState } from 'vuex' export default { // ... computed: mapState({ // Arrow functions make the code very concise count: state => state.count, // The incoming string'count'is equivalent to `state = > state. count'. countAlias: 'count', // To access the local state, we must use a common function to get the local state by using `this'. countPlusLocalState (state) { return state.count + this.localCount } }) }
When the name of the computed attribute corresponds to the name of the state subtree, we can pass in an array of strings to the mapState utility function.
computed: mapState([ // Map this.count to this.$store.state.count 'count' ])
Through examples, we can see intuitively that the mapState function can accept either an object or an array. What on earth does it do at the bottom? Let's take a look at the definition of the function of source code.
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 }
The function first calls the normalizeMap method for the incoming parameters. Let's look at the definition of this function:
function normalizeMap (map) { return Array.isArray(map) ? map.map(key => ({ key, val: key })) : Object.keys(map).map(key => ({ key, val: map[key] })) }
This method determines whether the parameter map is an array. If it is an array, it calls the map method of the array to convert each element of the array into an object {key, val: key}; otherwise, the incoming map is an object (from the use scenario of mapState, the incoming parameter is either an array or an object). We call the Object.keys method to traverse the key of the map object. Converts each key of the array to an object {key, val: key}. Finally, we use this object array as the return value of normalizeMap.
Back to the mapState function, after calling the normalizeMap function, the incoming states are converted into an array composed of {key, val} objects. Then the forEast method is called to traverse the array and construct a new object. Each element of the new object returns a new function, mappedState. The function determines the type of val. If Val is a function, it calls directly. With this val function, the state and getters on the current store are taken as parameters, and the return value is taken as the return value of mappedState; otherwise, the return value of this.$store.state[val] is taken as the return value of mappedState directly.
So why is the return value of the mapState function such an object, because the function of the mapState is to map the global state and getters to the computed computational properties of the current component, and we know that each computational attribute in the Vue is a function.
To illustrate this more intuitively, let's go back to the previous example:
import { mapState } from 'vuex' export default { // ... computed: mapState({ // Arrow functions make the code very concise count: state => state.count, // The incoming string'count'is equivalent to `state = > state. count'. countAlias: 'count', // To access the local state, we must use a common function to get the local state by using `this'. countPlusLocalState (state) { return state.count + this.localCount } }) }
The result after calling the mapState function is as follows:
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 } } }
Let's look again at an example where the mapState parameter is an array:
computed: mapState([ // Map this.count to this.$store.state.count 'count' ])
The result after calling the mapState function is as follows:
computed: { count() { return this.$store.state['count'] } }
mapGetters
The mapGetters tool function maps getter s in the store to locally computed properties. Its function is very similar to that of mapState. Let's look directly at its implementation:
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 }
The implementation of mapGetters is similar to that of mapState, except that its value cannot be a function, but only a string, and it checks the value of val in this.$store.getters and outputs an error log for false. For a more intuitive understanding, let's look at a simple example:
import { mapGetters } from 'vuex' export default { // ... computed: { // Mix getter into computer using object extension operators ...mapGetters([ 'doneTodosCount', 'anotherGetter', // ... ]) } }
The result after calling the mapGetters function is as follows:
import { mapGetters } from 'vuex' export default { // ... computed: { doneTodosCount() { return this.$store.getters['doneTodosCount'] }, anotherGetter() { return this.$store.getters['anotherGetter'] } } }
Let's look at another example where the parameter mapGetters is an object:
computed: mapGetters({ // Map this.doneCount to store.getters.doneTodosCount doneCount: 'doneTodosCount' })
The result after calling the mapGetters function is as follows:
computed: { doneCount() { return this.$store.getters['doneTodosCount'] } }
mapActions
The mapActions tool function maps the dispatch method in the store to the method of the component. Similar to mapState and mapGetters, it maps not to compute attributes, but to methods objects of components. Let's look directly at its implementation:
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 }
As you can see, the implementation routine of functions is similar to that of mapState and mapGetters, or even simpler. In fact, it is a layer of function wrapping. For a more intuitive understanding, let's look at a simple example:
import { mapActions } from 'vuex' export default { // ... methods: { ...mapActions([ 'increment' // Map this.increment() to this.$store.dispatch('increment') ]), ...mapActions({ add: 'increment' // Map this.add() to this.$store.dispatch('increment') }) } }
The result after calling the mapActions function is as follows:
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
The map Mutations tool function maps the commit method in the store to the method of the component. Almost the same as mapActions, let's look directly at its implementation:
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 }
Function implementations are almost the same as mapActions. The only difference is that the store commit method is mapped. For a more intuitive understanding, let's look at a simple example:
import { mapMutations } from 'vuex' export default { // ... methods: { ...mapMutations([ 'increment' // Map this.increment() to this.$store.commit('increment') ]), ...mapMutations({ add: 'increment' // Map this.add() to this.$store.commit('increment') }) } }
The result after calling the mapMutations function is as follows:
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)) } } }
Plug-in unit
The store of Vuex receives the plugins option. A Vuex plug-in is a simple way to receive the store as the only parameter. Plug-ins are usually used to monitor every mutation and do something.
At the end of the constructor of the store, we call the plug-in through the following code:
import devtoolPlugin from './plugins/devtool' // apply plugins plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
We usually call the logger plug-in when we instantiate the store. The code is as follows:
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()] : [] })
In these two examples, we call devtoolPlugin and createLogger () plug-ins, which are built-in Vuex plug-ins. Let's look at their implementations next.
devtoolPlugin
The main function of devtool Plugin is to display the status of Vuex by using the developer tools of Vue and Vuex. Its source code is in src/plugins/devtool.js, so let's see what the plug-in actually does.
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) }) }
From the point of view of the exposed devtoolPlugin function, the function first judges the value of devtoolHook. If our browser is equipped with the Vue developer tool, then there will be a u VUE_DEVTOOLS_GLOBAL_HOOK_ reference on window s, then the devtoolHook will point to this reference.
Next, a Vuex initialization event is dispatched through devtoolHook.emit('vuex:init', store), so that the developer tool can get the current store instance.
Next, the traval-to-state event of Vuex is monitored by devtoolHook.on ('vuex: travel-to-state', targetState => {store.replaceState (targetState)}), which replaces the current state tree with the target state tree. This function also replaces the state of Vuex with the Vue developer tool.
Finally, the store state is subscribed to by the store. subscribe ((mutation, state) => {devtoolHook. emit ('vuex: mutation', mutation, state)} method. When the store mutation submits a change in state, a callback function is triggered - an event of Vuex mutation is dispatched through devtoolHook, mutation and rootState are taken as parameters, so that the developer's tool can Real-time changes in Vuex state can be observed, and the latest status tree can be displayed on the panel.
loggerPlugin
Usually in a development environment, we want to output mutation actions and store state changes in real time, so we can use logger Plugin to help us do this. Its source code is in src/plugins/logger.js, so let's see what the plug-in actually does.
// 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 }
The plug-in exposes the createLogger method, which actually accepts three parameters, all of which have default values. Usually, we can use the default values. CreateLogger returns a function that actually executes when I execute the logger plug-in. Here's what this function does.
The function first executes let prevState = deepCopy(store.state) to deeply copy the rootState of the current store. Why do you want to copy it in depth here, because if it's a simple reference, any change in store.state will affect the reference, so you can't record a state. Let's look at the implementation of deepCopy, which is defined in 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 }
DepCopy is no stranger. Many open source libraries such as loadash and jQuery have similar implementations. The principle is not difficult to understand. It is mainly to construct a new object, traverse the original object or array, and recursively call deepCopy. However, the implementation here has an interesting point: every time deepCopy is executed, the current nested objects are cached with cache arrays, and the copy returned by deepCopy is executed. If a circular reference is found through find (cache, C => c.original === obj) during the deepCopy process, the corresponding copy in the cache is returned directly, thus avoiding the infinite loop.
Back to the loggerPlugin function, a copy of the current state is copied through deepCopy and saved with the prevState variable. Next, the store.subscribe method is called to subscribe to the change of the store's state. In the callback function, a copy of the current state is obtained through the deepCopy method and saved with the nextState variable. Next, we get the string of mutation changes that have been formatted at the current formatting time. Then we use console.group and console.log grouping to output prevState, mutation and nextState. Here, we can control the display effect of our final log through the parameters collapsed, transformer and mutation transformer of our createLogger. At the end of the function, we assign nextState to prevState for the next mutation.