Vuex is a dedicated Vue ecosystem for managing page data state and providing unified data manipulation. It focuses on the Model layer in THE MVC pattern, and stipulates that all data operations must be carried out through the process of action-mutation – state change, combined with the two-way binding feature of Vue data view to achieve page display update. Unified page state management and operation processing make complex component interaction simple and clear. Meanwhile, you can perform backward operation like time machine in debugging mode to view data change process and make code debug more convenient.
Recently, Vuex was used in the development project to manage the overall page state, and many problems were encountered. Decided to study the source code, in addition to answer questions, can in-depth study its implementation principle.
Put the question out first to make learning and research more targeted:
- With Vuex you just need to execute
Vue.use(Vuex)
And pass an example of a Store object into the Vue configuration. How does store implement injection? - How is state internally implemented to support module configuration and module nesting?
- When executing the dispatch action (commit), you only need to pass in (type, payload). Where does the first parameter of the action execution function store come from?
- How do I distinguish whether state was modified directly externally or by mutation method?
- How is the “time travel” feature implemented during debugging?
Note: This paper is of great help to students who have practical experience in using Vuex. They can understand the working process and principle of Vuex more clearly and use it more easily. For the first time, students can learn basic concepts by referring to Vuex official documents.
First, the framework core process
Before analyzing the source code, take a look at the core idea diagram provided in the official documentation, which represents the flow of the Vuex framework.
As illustrated, Vuex establishes a complete ecosystem for Vue Components, including API calls under development. Around this ecosystem, the main functions of each module in the core process are briefly introduced:
- Vue Components: Vue Components. On the HTML page, responsible for receiving user operations and other interactive behaviors, execute the Dispatch method to trigger corresponding actions to respond.
- Dispatch: Action action trigger method, the only method that can execute an action.
- Actions: Action handling module. Responsible for handling all interactions received by Vue Components. Contains synchronous/asynchronous operations and supports multiple methods of the same name that are triggered in the order of registration. Requests to the background API are made in this module, including triggering other actions and submitting mutations. This module provides the encapsulation of promises to support the chain firing of actions.
- Commit: State changes commit operation method. Committing for mutation is the only way to perform mutation.
- Mutations open the door. Is the only recommended method for Vuex to modify state. Other modification methods will report errors in strict mode. This method can only perform synchronous operations, and the method name must be globally unique. Some hooks will be exposed during the operation for state monitoring and so on.
- State: page state management container object. Centralized storage of scattered data objects in Vue Components, globally unique, for unified state management. The data required for page display is read from this object, leveraging Vue’s fine-grained data response mechanism for efficient status updates.
- Getters: State Object reading method. This module is not listed separately and should be included in Render, where Vue Components read the global state object.
After receiving the interaction behavior, the Vue component called the Dispatch method to trigger the relevant action processing. If the page state needed to be changed, it called the commit method to submit the mutation and modify the state, obtained the new state value through getters, and re-rendered the Vue Components. The interface is updated.
2. Introduction to directory structure
Open the Vuex project and take a look at the source directory structure.
Vuex provides a very powerful state management function with a small amount of source code and a clear directory structure. Firstly, the functions of each directory file are introduced:
- Module: Provides module object and module object tree creation function;
- Plugins: provide development assistance plug-ins, such as “time travel” function, state change logging function, etc.
- Helpers. js: provides the search API for Action, mutations, and getters;
- Index.js: is the main source entry file, provides the store of each module to build installation;
- Mixin.js: provides store loading injection on Vue instances;
- Util.js: Provides utility methods such as find, deepCopy, forEachValue, and Assert.
Three, initial loading and injection
After understanding the general directory and corresponding functions, the following start source analysis. Index.js contains all the core code, start with this file for analysis.
3.1 Loading Examples
Let’s start with a simple example:
/** * store. Js file * create store object, Configure state, action, mutation, and getter ** */ import Vue from 'Vue' import Vuex from 'Vuex' // install Vuex framework vue. use(Vuex) // Create and export a Store object. Export default new vuex.store () is not configured.Copy the code
In the store.js file, load the Vuex framework, create and export an empty configured store object instance.
/** * vue-index.js ** **/ import vue from 'vue' import App from './.. /pages/app.vue' import store from './store.js' new Vue({ el: '#root', router, store, render: h => h(App) })Copy the code
Then, in index.js, you normally initialize a page-root-level Vue component, passing in the custom Store object.
As mentioned in question 1, the above example has nothing more than the initialization code for the Vue and a store object passed in. Take a look at the implementation of the source code.
3.2 Loading analysis
The index.js file begins the code execution by defining local Vue variables that determine whether or not global scope lookups have been loaded.
let Vue
Copy the code
Then determine if you are in a browser environment and Vue has been loaded, execute the install method.
// auto install in dist mode if (typeof window ! == 'undefined' && window.Vue) { install(window.Vue) }Copy the code
The install method loads Vuex into the Vue object, and vue. use(Vuex) is also executed through it. Let’s look at the vue. use method:
function (plugin: Function | Object) { /* istanbul ignore if */ if (plugin.installed) { return } // additional parameters const args = toArray(arguments, 1) args.unshift(this) if (typeof plugin.install === 'function') {plugin.install.apply(plugin, plugin); args) } else { plugin.apply(null, args) } plugin.installed = true return this }Copy the code
For the first time, assign the local Vue variable to a global Vue object and execute the applyMixin method. Install implements the following:
function install (_Vue) {
if (Vue) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
return
}
Vue = _Vue
applyMixin(Vue)
}
Copy the code
Look at the code inside the applyMixin method. For versions above 2.x.x, you can use hook injection or the _init method that encapsulates and replaces the Vue object prototype.
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
Copy the code
Implementation: set the store passed in when initializing the Vue root component to the $store property of this object, and the child component references the $store property from its parent component, nesting layer upon layer for setting. This.$store in any component will find the loaded store object. VuexInit is implemented as follows:
function vuexInit () { const options = this.$options // store injection if (options.store) { this.$store = options.store } else if (options.parent && options.parent.$store) { this.$store = options.parent.$store } }Copy the code
Let’s look at an illustration to understand store delivery.
Page Vue structure diagram:
Corresponding store flow direction:
4. Store object construction
The above analysis of Vuex framework loading and injection of custom store objects solves problem 1. Next, analyze the internal functions and concrete implementation of the Store object in detail, to answer why actions, getters, mutations can get the relevant data of the store from arguments[0]? Wait for a problem.
Store object implementation logic is complex, first look at the overall logic flow of the constructor to help understand the following:
4.1 Environment Judgment
Start analyzing the Store constructor, function by function, line by line, section by section.
constructor (options = {}) { assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`) assert(typeof Promise ! == 'undefined', `vuex requires a Promise polyfill in this browser.`)Copy the code
The following are necessary conditions for Vuex to work when performing environment judgments in the Store constructor:
- The install function has been executed to load;
- Support the Promise syntax.
The Assert function is a simple assertion function implementation that can be implemented in a single line of code.
function assert (condition, msg) { if (! condition) throw new Error(`[vuex] ${msg}`) }Copy the code
4.2 Data initialization and Module tree construction
After the environment determines, the internal data is initialized by constructing options or default values passed in according to new.
const { state = {}, plugins = [], Strict = false} = options // store internal state this. _case = false // Indicate whether the commit status is happening (this._actions =) _mutations = object. create(null) // Acitons operation Object this._mutations = object. create(null) // mutations operation Object this._wrappedGetters = Object.create(null) // This._modules = new ModuleCollection(options) // Vuex supports store modules. Store modules this._ModulesNamespacemap = object.create (null) // Module namespace map this._subscribers = [] Vuex provides a subscribe function this._watchervm = new Vue() // The Vue component is used by watch to monitor changesCopy the code
The options object passed in when new vuex.store (options) is called, which is used to construct the ModuleCollection class.
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.root = new Module(rawRootModule, false)
// register all nested modules
if (rawRootModule.modules) {
forEachValue(rawRootModule.modules, (rawModule, key) => {
this.register([key], rawModule, false)
})
}
Copy the code
ModuleCollection constructs the entire options object as a module object and loops through this.register([key], rawModule, false) to register modules properties. Make them all module objects, and finally the Options object is constructed as a complete tree of components. The ModuleCollection class also provides an alternative to modules. For details, see the source file module-collection.js.
4.3 Dispatch and Commit Settings
Go back to the store constructor code.
// 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)
}
Copy the code
Encapsulate the dispatch and Commit methods in the replacement prototype, pointing this to the current Store object. The dispatch and commit methods are implemented as follows:
dispatch (_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_type, _actions[type] if (!) const entry = this._actions[type] if (! entry) { console.error(`[vuex] unknown action type: ${type}`) return } return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload) }Copy the code
As mentioned earlier, the function of dispatch is to trigger and pass parameters (payload) to the action of the corresponding type. Since it supports two invocation methods, the parameters are first adapted in Dispatch, and then the action types are determined to exist. If they exist, they are executed one by one (Note: This. _actions[type] and this._mutations[type] in the code above are all collections of functions that have been processed, and the detailed content will be left for later analysis.
Although the commit method is the trigger type compared with dispatch, the corresponding processing is relatively complex. The code is as follows.
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) { console.error(`[vuex] unknown mutation type: ${type} ') return} This._withcommit (() => {entry.forEach(function commitIterator (handler) {handler(payload)})}) // The subscriber function traverses the execution, ForEach (sub => sub(mutation, this.state)) if (options && options.silent) { console.warn( `[vuex] mutation type: ${type}. Silent option has been removed. ` + 'Use the filter functionality in the vue-devtools' ) } }Copy the code
This method also supports two invocation methods. First, parameter adaptation was performed to determine the mutation type, and the _withCommit method was used to execute the batch mutation processing function, and the payload parameter was passed in. On completion, all _subscribers are notified of the mutation object and its current state, and are alerted if a removed Silent option is passed in.
4.4 State Modification method
_withCommit is a proxy method through which all mutation triggering state changes are performed to centrally manage and monitor state changes. The implementation code is as follows.
(withcommit (fn) {" save the commit state (const research (true) = this. _research ()) ") "Vuex" will generate an illegal state change warning this._research = true (right) // Perform the state change operation "fn()" // Complete the change, restore the state before this change "this._research = research}"Copy the code
Set the current state to True (think of this case) for this commit, and restore the state (think of this case).
4.5 the module installation
After the Dispatch and COMMIT methods are bound, the strict mode is set and the module is installed. The strict mode is recommended to be enabled only in development mode because it consumes too many resources and affects page performance.
// strict mode
this.strict = strict
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
Copy the code
4.5.1 Initializing rootState
In the comments above, which mention that the installModule method initializes the component root component, registers all of its children, and stores all of its getters to the this._wrappedgetters property, let’s take a look at the code implementation.
function installModule (store, rootState, path, module, hot) { const isRoot = ! path.length const namespace = store._modules.getNamespace(path) // register in namespace map if (namespace) { Store. _modulesNamespaceMap[namespace] = module} isRoot && ! hot) { const parentState = getNestedState(rootState, path.slice(0, -1)) const moduleName = path[path.length - 1] store._withCommit(() => { Vue.set(parentState, moduleName, The module. The state)})} · · · · · ·Copy the code
Check whether it is the root directory and whether the namespace is set. If it exists, store the Module in the namespace. If it is not the root component and not hot, get the state of the parent of the Module through getNestedState. Set (parentState, moduleName, module.state) and set its state to the moduleName property of the parentState object. This implements the module’s state registration (the first time this is done, the method in this condition is not executed because it is a root directory registration). The getNestedState method is simple, parsing path to get state as follows.
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
Copy the code
4.5.2 Setting context for Module
const local = module.context = makeLocalContext(store, namespace, path)
Copy the code
Once the namespace and root conditions are determined, define the local variable and the value of module.context, execute the makeLocalContext method, Set local Dispatch and commit methods, as well as getters and state for the Module (compatibility is required due to namespace).
4.5.3 Mutations, Actions and getters register
After the local environment is defined, we iterate to register the actions and mutations we configured in options. Before analyzing each registration function one by one, take a look at the flow chart of the logical relationship between modules:
Here’s the code logic:
// register the mutation of the corresponding module, Module. forEachMutation((mutation, key) => {const namespacedType = namespace + key registerMutation(store, NamespacedType, mutation, local)}) Module. forEachAction((action, key) => { const namespacedType = namespace + key registerAction(store, namespacedType, action, Local)}) // Register the corresponding module getters, Module. forEachGetter((getter, key) => {const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) })Copy the code
In the registerMutation method, the set of processing functions corresponding to the mutation type in the store is obtained and the new processing functions are pushed into it. The corresponding handler that we set up on mutations Type is encapsulated here, passing state to the original function. When a commit(‘ XXX ‘, payload) is performed, all mutation handlers of type XXX receive the state and payload. This is why the state is retrieved from the handler.
function registerMutation (store, type, handler, Local) {/ / take out the corresponding type of mutations - handler collection const entry = store. _mutations [type] | | (store) _mutations [type] = []) / / Commit actually calls not the handler we passed in, Push (function wrappedMutationHandler (payload) {// Call handler and pass state to handler(local.state, payload)})}Copy the code
The same applies to the registration of actions and getters, so take a look at the code (note: the aforementioned this.actions and this.mutations are set here).
function registerAction (store, type, handler, Local) {/ / take out the corresponding type of actions - handler collection const entry = store. _actions [type] | | (store) _actions [type] = []) / / Store new encapsulated action-handler entries. Push (function wrappedActionHandler (payload, Cb) {let res = handler({dispatch: local.dispatch, commit: local. MIT, 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 } }) } function registerGetter (store, Type, rawGetter, local) {// getters allows only one handler, If (store._wrappedgetters [type]) {console.error(' [vuex] duplicate getter key: ${type} ') return} // Store the wrapped getters handler store._wrappedgetters [type] = function wrappedGetter (store) {// Return rawGetter(local.state, // local state local.getters, // local getters store.state, // local getters store. // root state store.getters // root getters ) } }Copy the code
The Action handler gets more dispatch and COMMIT operations than the mutation handler and getter wrapper, so the Action can perform both the Dispatch action and commit mutation operations.
4.5.4 Installing subModules
After registering the root component’s Actions, Mutations, and getters, it recursively calls itself to register its state, Actions, mutations, and getters for its children.
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
Copy the code
4.5.5 Example Combination
The implementation of the Dispatch and Commit methods, actions, and so on was introduced earlier, with some code from an official shopping cart instance.
Vuex configuration code:
/ * store-index.js store configuration file * / import Vue from 'Vue' import Vuex from 'Vuex' import * as actions from './actions' import * as getters from './getters' import cart from './modules/cart' import products from './modules/products' import createLogger from '.. /.. /.. /src/plugins/logger' Vue.use(Vuex) const debug = process.env.NODE_ENV ! == 'production' export default new Vuex.Store({ actions, getters, modules: { cart, products }, strict: debug, plugins: debug ? [createLogger()] : [] })Copy the code
State configuration code of each module in Vuex component Module:
/**
* cart.js
*
**/
const state = {
added: [],
checkoutStatus: null
}
/**
* products.js
*
**/
const state = {
all: []
}
Copy the code
After loading the above configuration, the page state structure is shown as follows:
Properties in state are configured according to the rules for Module Path in option configuration. See action for an example.
Vuecart component code section:
/** */ export default {methods: {// The purchase button in the Cart will trigger the settlement. Checkout (products) {this.$store.dispatch('checkout', products)}}}Copy the code
Vuexcart.js component Action configuration code section:
const actions = { checkout ({ commit, state }, Products) {const savedCartItems = [...state.added] // Store items added to cart COMMIT (types.checkout_request) // Set commit settlement status Shop.buyproducts (// submit API request, () => commit(types.checkout_success), CHECKOUT_FAILURE => commit(types.checkout_failure, {savedCartItems})Copy the code
Execute the dispatch method of the current Module by pressing the purchase button on the Vue component. Pass in type = ‘checkout’, payload = ‘products’, The dispatch method in the source code looks for the corresponding execution array for ‘checkout’ in all the registered actions and retrieves the loop execution. It executes a wrapped method named wrappedActionHandler. The actual checkout execution function that is passed in is executed in the wrappedActionHandler method, and the source code is as follows:
function wrappedActionHandler (payload, cb) { let res = handler({ 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 } }Copy the code
This is where the checkout function is passed in. The commit and state required for executing it are passed in. The payload is also passed in. CHECKOUT_REQUEST (CHECKOUT_REQUEST); CHECKOUT_REQUEST (CHECKOUT_REQUEST); CHECKOUT_REQUEST (CHECKOUT_REQUEST); Call function wrappedMutationHandler (payload) {handler(local.state, payload)} to wrap the function to actually call the configured mutation method.
After reading the source code analysis and the sample above, you should understand how the Dispatch Action and COMMIT mutation work. Take a look at the source code to see how Getters implements real-time state access.
4.6 Store. _VM Component Settings
After installing each module, execute the resetStoreVM method to initialize the Store component.
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
Copy the code
Based on the previous analysis, it can be seen that Vuex actually builds a VM component named Store. All configuration state, actions, mutations and getters are attributes of its component, and all operations are carried out on this VM component.
Take a look at the internal implementation of the resetStoreVM method.
function resetStoreVM (store, State) {const oldVm = store._vm // Cache front VM component // bind store public getters store.getters = {} const wrappedGetters = Store._wrappedGetters const computed = {} // Loop through all processed getters and create a new computed Object to store, using object.defineProperty to create attributes for getters, Getters forEachValue(wrappedGetters, (fn,)) lets getters forEachValue(wrappedGetters, (fn,)) be accessed by this.$store.getters. key) => { // use computed to leverage its lazy-caching mechanism computed[key] = () => fn(store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) }) // use a Vue instance to store the state tree // suppress warnings just in case the user Added // some funky global mixins const silent = vue.config. silent Vue. Config. silent = true // Set new storeVm Store._vm = new Vue({data: { state }, Computed}) // Restore the Vue mode vue.config. silent = silent // enable strict mode for new VM if (store.strict) {// State enableStrictMode(store)} // If this method is not performed during initialization, set the old component state to NULL, Force all Watchers to update. After the update takes effect and DOM update is completed, execute the DESTROY method of VM component to destroy it. If (oldVm) {// Dispatch changes in all subscribed watchers // to force getter re-evaluation. store._withCommit(() => { oldVm.state = null }) Vue.nextTick(() => oldVm.$destroy()) } }Copy the code
The resetStoreVm method creates the _VM component of the current store instance, and the store is now created. The code above deals with strict pattern judgment. let’s see how strict pattern is implemented.
function enableStrictMode (store) {
store._vm.$watch('state', () => {
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}, { deep: true, sync: true })
}
Copy the code
A simple application that monitors state changes, and an error is reported if the state is not changed using this._withcommit ().
4.7 the plugin into
Finally, the plugin is implanted.
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
Copy the code
The devtoolPlugin provides three functions:
Emit hook devtoolhook. emit(' Vuex :init', store) // 2. Provides a "time travel" function, Devtoolhook. on('vuex:travel-to-state', TargetState => {store.replacestate (targetState)}) // 3. Subscribe ((mutation, state) => {devtoolhook. emit('vuex:mutation', mutation, state) })Copy the code
Source code analysis to here, Vuex framework of the implementation of the basic principles have been analyzed.
Five, the summary
Finally, let’s go back to the five questions raised at the beginning of the article.
1. Q: To use Vuex, simply execute vue.use (Vuex) and pass in an example store object in the configuration of Vue. How does store implement injection?
A: The vue.use (Vuex) method performs the install method, which implements the init method encapsulation and injection of Vue instance objects, so that the store object passed in is set to the $store in the Vue context. Therefore, the store can be accessed anywhere in the Vue Component through this.$store.
2. Q: State supports module configuration and module nesting internally. How to achieve this?
A: In the Store constructor there is the makeLocalContext method. All modules have a local context that matches the path at configuration time. When executing an action such as dispatch(‘submitOrder’, payload), the default value is the local state of the module. If you want to access the state of the outermost module or another module, Access can only be done step by step from rootState to path.
3. Q: When executing the dispatch action(commit), we only need to pass in (type, payload). Where does the first parameter of the action execution function store come from?
Answer: All configured action and mutation and getters are encapsulated when the store is initialized. For dispatch(‘submitOrder’, payload), all actions whose type is submitOrder are encapsulated. The first parameter is the current store object. Therefore, data such as {dispatch, COMMIT, state, rootState} can be obtained.
4. Q: How does Vuex distinguish whether state is modified directly externally or by mutation method?
A: The only way to change state in Vuex is to call commit(‘xx’, payload), which sets the _research flag variable to true by calling this._withcommit (fn), and then change state, You also need to restore the _research variable. External changes can change state directly, but they don’t change the _research flag (right), so just watch state and determine whether the _research value is true (right) when state change.
5. Q: How is the “time travel” function implemented during debugging?
A: This is provided in the devtoolPlugin. Since all state changes are recorded in Dev mode, the ‘time travel’ function essentially replaces the current state with the state at a point in time in the record, The this._vm.state = state implementation is performed using the store.replacEstate (targetState) method.
There are some tool functions such as registerModule, unregisterModule, hotUpdate, watch and SUBSCRIBE in the source code, if interested, you can open the source code to see, here is no longer detailed.
6. Introduction to the author
Ming Yi, senior front-end R&D engineer of Meituan Takeout, joined Meituan Takeout in 2014, responsible for the development of Web master site. After successively participating in the development of b-terminal, C-terminal, delivery and other whole-business line systems, I am now mainly responsible for the business voucher activity system.
Finally, attached is a hard advertisement. Meituan takeout is looking for senior front-end engineer/front-end technology expert. Welcome to send your resume to: mabingbing02#meituan.com.
If you answer “thinking questions”, find mistakes in the article, or have questions about the content, you can leave a message to us at the background of wechat public account (Meituan-Dianping technical team). Each week, we will select one “excellent responder” and give a nice small gift. Scan the code to pay attention to us!