Vuex 2.0 Source Analysis

Vuex 2.0 Source Analysis

In general, we use the global event bus to solve the problem of global state sharing and component communication. When encountering large applications, this will make the code difficult to maintain. Vuex came into being. Next, I will analyze the whole implementation of Vuex from the source code point of view.

directory structure


The directory structure of the whole Vuex is still very clear, index.js is the entry of the whole project, helpers.js provides the auxiliary method of Vuex >, mixin.js is the method of injecting $store into the vue instance, util.js is some tool functions, store.js is the implementation of store class, etc. Next, the whole source code is analyzed step by step from the entry of the project.

Project Entry

First we can look at index.js:

 export default {
    Store,
    install,
    version: '__VERSION__',
    mapState,
    mapMutations,
    mapGetters,
    mapActions,
    createNamespacedHelpers
 }

You can see that index.js just exports a Vuex object. Here you can see the API exposed by Vuex. Store is a state storage class provided by Vuex. Usually, it creates an instance of Vuex by using the new Vuex.Store(...) method. Next, install method, in store.js;

export function install (_Vue) {
       if (Vue && _Vue === Vue) {
            if (process.env.NODE_ENV !== 'production') {
                console.error(
                    '[vuex] already installed. Vue.use(Vuex) should be called only once.'
                )
            }
            return
        }
       Vue = _Vue
       applyMixin(Vue)
   }

The install method has a duplicate install detection error and assigns the incoming_Vue to its own defined Vue variable, which has been exported so that the entire project can use Vue instead of installing Vue;

 export let Vue

Next, call the applyMixin method, which is in mixin.js;

export default function (Vue) {
    const version = Number(Vue.version.split('.')[0])
    Vue.mixin({ beforeCreate: vuexInit })
}

Therefore, the logic of the applyMixin method is to globally blend in a beforeCreate hook function, vuexInit;

function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
        this.$store = typeof options.store === 'function'
            ? options.store()
            : options.store
    } else if (options.parent && options.parent.$store) {
        this.$store = options.parent.$store
    }
}

The whole code is simple, injecting the stores passed in by the user into the $store property of each vue instance so that we can access the data and state of the Vuex by calling this.$store.xx on each instance.

Store Class

When we use Vuex, we usually instantiate a Vuex.Store class and pass in an object, including state, getters, mutations, actions, modules. What exactly did Vuex do when we instantiated it? With this question in mind, let's look at the code in store.js, first of all the constructors;


constructor (options = {}) {

    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
        install(window.Vue)
    }
    
    if (process.env.NODE_ENV !== 'production') {
        assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
        assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
        assert(this instanceof Store, `store must be called with the new operator.`)
    }
    
    const {
        plugins = [],
        strict = false
    } = options
    
    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()
    
    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
    
    this.dispatch = function boundDispatch (type, payload) {
        return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
        return commit.call(store, type, payload, options)
    }
    // strict mode
    this.strict = strict
    const state = this._modules.root.state
   
    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], this._modules.root)
    
    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)
    
    // apply plugins
    plugins.forEach(plugin => plugin(this))
    
    const useDevtools = options.devtools !== undefined ? options.devtools :                     Vue.config.devtools
    if (useDevtools) {
        devtoolPlugin(this)
    }
}

The constructor starts by judging that when window.Vue exists, it calls the install method to ensure that the Vuex loaded by script can be installed correctly, followed by three assertion functions to ensure that the Vue exists, that the environment supports Promise, and that the this of the current environment is Store.

const {
    plugins = [],
    strict = false
} = options

Use the es6 assignment structure to get the plugins (default is []), strict (default is false), plugins to indicate whether the application plugins, strict to indicate whether strict mode is turned on, and then look down;

// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()

This is mainly to initialize some attributes inside the Vuex, begins, which generally represents private attributes.
this._committing marks a submission status;
this._actions stores all actions of the user;
this.mutations stores all mutations owned by the user;
this.wrappedGetters stores all the getters owned by the user;
this._subscribers stores all subscribers to mutation changes;
this._modules represents a collection of all modules;
this._modulesNamespaceMap represents the submodule name record.
Continue to look down:

// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
    return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
    return commit.call(store, type, payload, options)
}
// strict mode
this.strict = strict
const state = this._modules.root.state

This code gets the dispatch, commit method of the store object through the assignment structure, and redefines the dispatch, commit method of the store so that their this point to the instance of the store. The specific dispatch and comiit implementations will be analyzed later.

Vuex Core

installModule method

The installModule method installs and registers each module according to the options passed in by the user as follows:

function installModule (store, rootState, path, module, hot) {
    const isRoot = !path.length
    const namespace = store._modules.getNamespace(path)
    
    // register in namespace map
    if (module.namespaced) {
        if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
        store._modulesNamespaceMap[namespace] = module
    }
    
    // set state
    if (!isRoot && !hot) {
        const parentState = getNestedState(rootState, path.slice(0, -1))
        const moduleName = path[path.length - 1]
        store._withCommit(() => {
            Vue.set(parentState, moduleName, module.state)
        })
    }
    
    const local = module.context = makeLocalContext(store, namespace, path)
    
    module.forEachMutation((mutation, key) => {
        const namespacedType = namespace + key
        registerMutation(store, namespacedType, mutation, local)
    })
    
    module.forEachAction((action, key) => {
        const type = action.root ? key : namespace + key
        const handler = action.handler || action
        registerAction(store, type, handler, local)
    })
    
    module.forEachGetter((getter, key) => {
        const namespacedType = namespace + key
        registerGetter(store, namespacedType, getter, local)
    })
    
    module.forEachChild((child, key) => {
        installModule(store, rootState, path.concat(key), child, hot)
    })
}

The installModules method needs to pass in five parameters, store, rootState, path, module, hot; store refers to the current Store instance, rootState is the state of the root instance, path array of the current submodule, module refers to the current installation module, and hot is true when the module is dynamically changed or hot updated.

First look at this code:

if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
    console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
}

This code is designed to prevent duplicate naming of the sub-modules, so a map is defined to record each sub-module.

Next, look at the following code:

// set state
if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
        Vue.set(parentState, moduleName, module.state)
    })
}

This judges the case when it is not a root and not a hot update, and then sets up a cascade state. This is not easy to understand at first. Let's put it aside first and review later.

Look further down at the code:

const local = module.context = makeLocalContext(store, namespace, path)

First, define a local variable to receive the results returned by the makeLocalContext function. MakeLocalContext has three parameters. store refers to the root instance, namespace refers to the namespace character, and path is an array of paths.

function makeLocalContext (store, namespace, path) {
    const noNamespace = namespace === ''
    const local = {
        dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
            const args = unifyObjectStyle(_type, _payload, _options)
            const { payload, options } = args
            let { type } = args
            if (!options || !options.root) {
                type = namespace + type
                if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
                    console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
            return
            }
        }
        return store.dispatch(type, payload)
    },
    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
        const args = unifyObjectStyle(_type, _payload, _options)
        const { payload, options } = args
        let { type } = args
        if (!options || !options.root) {
            type = namespace + type
            if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
                console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
            return
            }
         }
        store.commit(type, payload, options)
        }  
    }
    // getters and state object must be gotten lazily
    // because they will be changed by vm update
    Object.defineProperties(local, {
        getters: {
            get: noNamespace
            ? () => store.getters
            : () => makeLocalGetters(store, namespace)
        },
        state: {
            get: () => getNestedState(store.state, path)
        }
    })
    return local
}

The main function of the makeLocalContext function is to define different dispatch es and commit s depending on whether a namespce exists, and to listen for the get properties of local getters and sate s. Where does the namespace come from, starting with the installModule:

const isRoot = !path.length
const namespace = store._modules.getNamespace(path)

namespace is obtained from the path array through getNamespace in _modules, and store._modules is an instance of ModuleCollection, so the getNamespace method can be found in ModuleCollection:

getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
        module = module.getChild(key)
        return namespace + (module.namespaced ? key + '/' : '')
    }, '')
}

This function traverses through the path path path array reduce to get the module's namespace (eg:'city/'); next is the registration process for each module, first look at the registration of mutaiton;

module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
})

The forEachMutation function iterates through a loop to get the mutation function and key value passed in by the user, and then calls the registerMutation function.

// $store.state.commit('add', 1)
function registerMutation (store, type, handler, local) {
    const entry = store._mutations[type] || (store._mutations[type] = [])
    entry.push(function wrappedMutationHandler (payload) {
        handler.call(store, local.state, payload)
    })
}

The purpose of this code is to encapsulate all mutation functions into wrappedMutation Handler and store it in store._mutations, which we can better understand by combining the commit process mentioned earlier.

commit (_type, _payload, _options) {
    // check object-style commit
    const {
    type,
    payload,
    options
    } = unifyObjectStyle(_type, _payload, _options)
    
    const mutation = { type, payload }
    const entry = this._mutations[type]
    
    if (!entry) {
        if (process.env.NODE_ENV !== 'production') {
            console.error(`[vuex] unknown mutation type: ${type}`)
        }
        return
    }
    
    this._withCommit(() => {
        entry.forEach(function commitIterator (handler) {
            handler(payload)
        })
    })
    
    this._subscribers.forEach(sub => sub(mutation, this.state))
    
    if (
    process.env.NODE_ENV !== 'production' &&
    options && options.silent
    ) {
        console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
        )
    }
}

The unifyObjectStyle function is the specification of the parameter, and then passes the `
this._mutations[type] takes all wrappedMutation Handler functions corresponding to the type, iterates through them, passes in payload, this._withCommit` function appears in the source code many times, code as follows:

_withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
}

The purpose of the code is to set this._committing to true each time a submission is made, reset to the initial state after a submission is made, make sure that only mutation can change the value of the state, and _subscribers-related code is temporarily unavailable. Let's take a look at the registration process for action:

module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
})

This code is similar to the registration process for mutation, but different from the registerAction function

function registerAction (store, type, handler, local) {
    const entry = store._actions[type] || (store._actions[type] = [])
    entry.push(function wrappedActionHandler (payload, cb) {
    
        let res = handler.call(store, {
            dispatch: local.dispatch,
            commit: local.commit,
            getters: local.getters,
            state: local.state,
            rootGetters: store.getters,
            rootState: store.state
        }, payload, cb)
        
        if (!isPromise(res)) {
            res = Promise.resolve(res)
        }
        
        if (store._devtoolHook) {
            return res.catch(err => {
                store._devtoolHook.emit('vuex:error', err)
                throw err
            })
        } else {
            return res
        }
    })
}

You can see that based on the user's action function, the source code is enclosed with a wrappedActionHandler function. In the action function, you can get a context object, which is the processing done here. Then, it encapsulates the result of the action function into Promise and returns, which is better understood with the dispatch function.

dispatch (_type, _payload) {
    // check object-style dispatch
    const {
        type,
        payload
    } = unifyObjectStyle(_type, _payload)
    
    const action = { type, payload }
    const entry = this._actions[type]
    
    if (!entry) {
        if (process.env.NODE_ENV !== 'production') {
            console.error(`[vuex] unknown action type: ${type}`)
        }
        return
    }
    
    const result = entry.length > 1
        ? Promise.all(entry.map(handler => handler(payload)))
        : entry[0](payload)
        
        return result.then(res => {
            return res
        })
}

When dispatch gets actions, it executes Promise.all or directly, depending on the length of the array, and then obtains the promise resolve result through the then function.

Next is the registration of getters

module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
})

RegiserGetter function:

function registerGetter (store, type, rawGetter, local) {
    // No duplication allowed
    if (store._wrappedGetters[type]) {
        if (process.env.NODE_ENV !== 'production') {
            console.error(`[vuex] duplicate getter key: ${type}`)
        }
        return
    }
    store._wrappedGetters[type] = function wrappedGetter (store) {
        return rawGetter(
            local.state, // local state
            local.getters, // local getters
            store.state, // root state
            store.getters // root getters
        )
    }
}

Wrap the rawGetter passed in by the user into a wrappedGetter and place it in the object of store._wrappedGetters. The function will be executed later and we will continue with the installation of the sub-module.

module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
})

This code first iterates over state.modules, calling installModule recursively, when the path is not an empty array, so it goes to this logic;

// set state
if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
        Vue.set(parentState, moduleName, module.state)
    })
}

Find its parent state through getNestedState, where the module key is the last item in the path, as explained above by store._withCommit, and then add the child module responsively to the parent state via Vue.set to register the child modules.

resetStoreVM method

resetStoreVM Functions Part One

const oldVm = store._vm

// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}

forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure                enviroment.
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
        get: () => store._vm[key],
        enumerable: true // for local getters
    })
})
    
    

First, get all wrappedGetter function objects, that is, wrapped getters passed in by the user, define a variable computed, accept all functions, and define get methods in the store.getters property by Ojbect.defineProperty, that is, we access the store._vm[xx] through this.$store.getters.xx, and store. _definePropertyWhat is vm?

// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
Vue.config.silent = true     // Turn off vue warning, alert

store._vm = new Vue({
    data: {
        $$state: state
    },
    computed
})

Vue.config.silent = silent

Obviously, store._vm is an instance of a Vue that contains both the computed properties of all user getters and the $$state property of user states, whereas we access this.$store.state is essentially the $$state property here, because the Store class directly defines a value function of a state, which returns the $$state property;

get state () {
    return this._vm._data.$$state
}

Let's go on;

// enable strict mode for new vm
if (store.strict) {
    enableStrictMode(store)
}

When in Vuex strict mode, the string is true, so the enableStrictMode function is executed;

function enableStrictMode (store) {
    store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
        assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
    }, { deep: true, sync: true })
}

This function uses the Vue.$watch function to listen for changes in $$state and throws a state that is not allowed to operate outside the mutation function if store._committing is false.

Next let's look at the last part.

if (oldVm) {
    if (hot) {
        // dispatch changes in all subscribed watchers
        // to force getter re-evaluation for hot reloading.
        store._withCommit(() => {
            oldVm._data.$$state = null
        })
    }
    Vue.nextTick(() => oldVm.$destroy())
}

oldVm holds a reference to the last store._vm object, and each time this function is executed, a new store._vm is created, so it needs to be destroyed in this code;

Now that you've roughly finished initializing the Store class, let's analyze the auxiliary functions provided by Vuex.

auxiliary function

mapstate
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) {
            const module = getModuleByNamespace(this.$store, 'mapState', namespace)
            if (!module) {
                return
            }
            state = module.context.state
            getters = module.context.getters
        }
        return typeof val === 'function'
            ? val.call(this, state, getters)
            : state[val]
        }
        // mark vuex getter for devtools
        res[key].vuex = true
    })
    return res
})

First, let's start with the normalizeMap method, which is mainly used to format parameters. Users can either pass in an array of strings or an object by using the mapState function. After processing by the normalizeMap method, an array of objects is returned uniformly.

// normalizeMap([1,2]) => [{key: 1, val: 1}, {key: 2, val: 2}] 
// normalizeMap({a: 1, b: 2}) => [{key: 'a', val: 1}, {key: 'b', val: 2}] 
function normalizeMap (map) {
    return Array.isArray(map)
        ? map.map(key => ({ key, val: key }))
        : Object.keys(map).map(key => ({ key, val: map[key] }))
}

Next, for the traversal of the processed object array, a res object receive is defined with key and mappedState method as value.

function mappedState () {
    let state = this.$store.state
    let getters = this.$store.getters

    if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
            return
        }
        state = module.context.state
        getters = module.context.getters
    }
    return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
}

The whole function code is simple, the only thing to note is that when namespace is passed in, the module corresponding to this property needs to be found through the getModuleByNamespace function. Remember that in the installModule, there is a corresponding relationship between namespace and module recorded in the store._modulesNamespaceMap, so getModuleByNamespace isIt is through this map that the module is found and the States and getters of the current module are obtained.

Finally, the mapstate function returns a res function object that the user can import directly into the calculated properties using the.. operator.

mapMutations

The mapMutations function is similar to the mapstate function except that mappedMutation is a proxy for the commit function and needs to be imported into methods.

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
    }
    return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
}

The implementation of mapActions and mapGetters are also very similar, so they are no longer analyzed in detail.

plugins option

We can use plugins in a similar way:

const myPlugin = store => {
    // Called when store is initialized
    store.subscribe((mutation, state) => {
        // Called after each mutation
        // mutation has the format {type, payload}
     }
 )}

const store = new Vuex.Store({
     // ...
     plugins: [myPlugin]
 })

In the source code, you can see a piece of code like this:

// apply plugins
plugins.forEach(plugin => plugin(this))

That is, iterate through all plugins, pass in the current Store instance, and execute the plugin function. Therefore, the store parameter of the example is the Store instance, and then the example calls the store.subscribe method, which is a member method exposed by the Store class.

subscribe (fn) {
    return genericSubscribe(fn, this._subscribers)
}

In fact, this is a subscription function that notifies all subscribers when there is a commit operation. The function returns a function, fn, which is called to cancel the subscription. The publish notification code is in the commit function:

this._subscribers.forEach(sub => sub(mutation, this.state))

epilogue

When learning nothing, looking at good sources may be a way to break through the bottleneck. You can get a better understanding of this library, know it, and know why. At the same time, some of the author's library design ideas will be of great benefit to us.

Keywords: Javascript Vue hot update

Added by chokies12 on Wed, 28 Aug 2019 05:12:02 +0300