Vuex 2.0 source code analysis

I. Preface

When we use Vue.js to develop a medium to large single-page application, we often encounter the following problems:

  • How to let multiple Vue components share state
  • How to communicate between Vue components

Usually, when the project is not very complex, we will use the global event bus to solve it, but as the complexity increases, these codes will become difficult to maintain. Therefore, we need a better solution, so Vuex was born.

This article is not a popular science article on Vuex. For those students who do not know Vuex, it is suggested to move to the official document of Vuex first.( http://vuex.vuejs.org/en/index.html Students who struggle to read English documents can see Vuex. Chinese Documents( https://vuefe.cn/vuex/).

The design idea of Vuex is inspired by Flux, Redux and The Elm Architecture. Its implementation is very ingenious and complements each other with Vue.js. Let's look at its implementation.

II. Directory Structure

The source code of Vuex is hosted in GitHub First, we will clone the code locally, select an IDE suitable for our own to open the source code, expand the src directory, as shown in the following figure:

There are not many files in the src directory, including several js files and plugins directory. The plugins directory contains two Vuex built-in plugins. The total source code is only 500-600 lines, which is a very lightweight library.

Although the sparrow is small and has all the internal organs, let's first intuitively feel the structure of the source code, and then look at the implementation details.

3. Source code analysis

The source code analysis process in this article will not be top-down annotations to the code. I prefer to analyze it from the API provided by Vuex and the way we use it. Vuex source code is based on ES6 grammar, for those who do not know ES6, it is recommended to learn ES6 first.

1. Start at the entrance

Look at the source code is usually from the entry, the entry of Vuex source code is src/index.js, first open this file.

Let's first look at the export of this library, at the end of the index.js code.

export default {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}

The exposed API s of Vuex can be seen at a glance here. Store is a state storage class provided by Vuex. Usually we use Vuex to create an instance of Store, which we will describe in detail later. Then there's the install method, which is usually a "routine" for writing third-party Vue plug-ins. Let's first look at the "routine" code:

function install (_Vue) {
  if (Vue) {
    console.error(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

We implemented an install method, which manually calls the install method when we refer to Vue globally, that is, when we have Vue objects on window s, and passes in the reference of Vue. When Vue is installed into the project through npm, we usually write the following code by introducing a third-party Vue plug-in into the code:

import Vue from 'vue'
import Vuex from 'vuex'
...
Vue.use(Vuex)

When we execute the code Vue.use(Vuex), we actually call the install method and pass in a reference to Vue. The install method, as its name implies, now let's look at its implementation. It accepts a parameter _Vue, which is first judged by the body of the function. This variable is defined at the beginning of the index.js file:

let Vue // bind on install

The judgment of Vue is mainly to ensure that the install method is executed only once. Here, the parameter _Vue object of the install method is assigned to the Vue variable, so that we can use the Vue variable elsewhere in the index.js file. At the end of the install method, the applyMixin method is called. Let's take a look at the implementation of this method, which is defined in the src/mixin.js file:

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)
    }
  }
  /**
   * Vuex init hook, injected into each instances init hooks list.
   */
  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
    }
  }
}

The purpose of this code is to insert a Vuex initialization code before the initialization hook in the lifecycle of Vue (version 1.0 is init, version 2.0 is beforeCreated). What we do here is simple -- inject a $store attribute into the instance of Vue, which is why we can access it in the component of Vue through this.$store.xxx Various data and states of Vuex.

2. Understanding Store constructors

When we use Vuex, we usually instantiate the Store class, and then pass in an object, including our defined actions, getters, mutations, state, and so on. Even when we have multiple sub-modules, we can add a module object. So what exactly did you do when you instantiated it? With that in mind, let's go back to the index.js file and focus on the definition of the Store class. The Store class defines a slightly longer code. I won't paste all the code in a minute. Let's break it down and analyze it. First, let's look at the implementation of the constructor.

class Store {
  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.`)
    const {
      state = {},
      plugins = [],
      strict = false
    } = options
    // store internal state
    this._options = options
    this._committing = false
    this._actions = Object.create(null)
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._runtimeModules = 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
    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], options)
    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)
    // apply plugins
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }
  ...
}  

The constructor starts with an "assertion function" to determine whether some conditions are met.

assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)

The purpose of this line of code is to ensure that Vue exists, that is, before we instantiate Store, we must ensure that the previous install method has been executed.

assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

The purpose of this line of code is to ensure that Promise can be used, because the source code of Vuex relies on Promise. Promise is a new API provided by ES6. Since not all browsers support ES6 grammar, we usually compile our code with babel. If we want to use Promise, we need to add a dependency on babel-polyfill in package.json and import'babel-polyfill'at the entry of the code.

Let's take a look at assert, which is not supported by browsers. It's implemented in src/util.js. The code is as follows:

export function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}

Very simple, if condition judgment is not true, then throw an exception. This function is simple, but it's worth learning.

Let's look at the following code for the constructor:

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

Here we use the structure assignment of es6 to get state, plugins and strict in options. State represents rootState, plugins represents the plug-in of the application, and string indicates whether strict mode is turned on.

Then look down:

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

Here are some internal attributes to create:

this._options stores parameter options.
this._committing flags a commit state, which ensures that changes to state in Vuex can only be made in the mutation callback function, but not outside at will.
this._actions is used to store all actions defined by the user.

this._mutations is used to store all user-defined mutations.
this._wrappedGetters is used to store all user-defined getters.
this._runtimeModules is used to store all runtime modules.

this._subscribers is used to store all subscribers for mutation changes.
this._watcherVM is an instance of a Vue object, mainly using the Vue instance method $watch to observe changes.

Keep looking 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
<div class="md-section-divider"></div>

The code here is not difficult to understand. The dispatch and commit methods of the Store class are pointed to the instance of the current store. The implementation of dispatch and commit will be analyzed later. this. string indicates whether to turn on strict mode, in which all state changes will be observed. It is recommended to turn on strict mode in the development environment, and turn off strict mode in the online environment, otherwise there will be some performance overhead.

3. Initialization Core of Vuex

(1)installModule

Let's go on to see:

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], options)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
<div class="md-section-divider"></div>

This code is the core of Vuex initialization, in which the installModule method registers and installs various attribute modules we pass in through options; the resetStoreVM method initializes store._vm, observes the changes of state and getters; and finally, the application of the incoming plug-in.

Next, let's look at the implementation of installModule:

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const {
    state,
    actions,
    mutations,
    getters,
    modules
  } = 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, state || {})
    })
  }
  if (mutations) {
    Object.keys(mutations).forEach(key => {
      registerMutation(store, key, mutations[key], path)
    })
  }
  if (actions) {
    Object.keys(actions).forEach(key => {
      registerAction(store, key, actions[key], path)
    })
  }
  if (getters) {
    wrapGetters(store, getters, path)
  }
  if (modules) {
    Object.keys(modules).forEach(key => {
      installModule(store, rootState, path.concat(key), modules[key], hot)
    })
  }
}
<div class="md-section-divider"></div>

The installModule function receives five parameters, store, rootState, path, module, hot. Store represents the current Store instance, rootState represents the root state, path represents the path array of the current nested module, module represents the currently installed module, and hot is true when module is dynamically changed or hot updated.

Let's first look at this part of the code:

const isRoot = !path.length
 const {
   state,
   actions,
   mutations,
   getters,
   modules
 } = module
<div class="md-section-divider"></div>

The code first determines whether it is a root by the length of the path array. We call the constructor installModule(this, state, [], options), so isRoot is true here. For the incoming options, we get the state, actions, mutations, getters and nested modules under the 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, state || {})
  })
}
<div class="md-section-divider"></div>

Here we judge the case of non-root and non-thermal updates, and then set up a cascade state. This is not easy to understand at first. Let's put it in first and review later.

Look further at the code:

if (mutations) {
  Object.keys(mutations).forEach(key => {
    registerMutation(store, key, mutations[key], path)
  })
}
if (actions) {
  Object.keys(actions).forEach(key => {
    registerAction(store, key, actions[key], path)
  })
}
if (getters) {
  wrapGetters(store, getters, path)
}
<div class="md-section-divider"></div>

Here are mutations, actions, getters for registration. If we instantiate Store by passing in these objects through options, then we will register them separately. I will introduce the specific implementation of registration later. So at this point, if Vuex does not have a module, the installModule method can be said to be done. But Vuex cleverly designed the concept of module, because Vuex itself is a single state tree, and all the states of the application are contained in a large object. As the scale of our application grows, the Store becomes very bloated. To solve this problem, Vuex allows us to store sub-module (module). Each module contains its own state, mutations, actions and getters, or even nested modules. So there's another line of code:

if (modules) {
  Object.keys(modules).forEach(key => {
    installModule(store, rootState, path.concat(key), modules[key], hot)
  })
}
<div class="md-section-divider"></div>

Here, installModule is recursively called to install the sub-module by traversing the modules. Here we pass in store, rootState, path.concat(key), and modules[key]. Unlike just now, path is not empty, module corresponds to sub-module, so let's go back to the previous 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, state || {})
  })
}
<div class="md-section-divider"></div>

When the submodule is initialized recursively, isRoot is false. Note that there is a method called getNestedState (root state, path). Let's look at the definition of getNestedState function:

function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}
<div class="md-section-divider"></div>

This method is very simple, which is to find nested states on state according to path. In this case, the rootState and path are passed in to calculate the state of the parent module of the current module. Since the path of the module is connected according to the name of the module concat, the last element of the path is the module name of the current module, and finally calls:

store._withCommit(() => {
  Vue.set(parentState, moduleName, state || {})
}) 
<div class="md-section-divider"></div>

Add the state of the current module to parentState.
Notice here that we used the store._withCommit method to look at the definition of this method:

_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}
<div class="md-section-divider"></div>

Since we are modifying the state, all changes to state in Vuex are wrapped in the _withCommit function to ensure that the value of this._committing is always true in the process of synchronous state modification. So when we observe the change of state, if the value of this._committing is not true, you can check that the state modification is problematic.

See here, some students may be a little confused, give an example to intuitively feel, take example/shopping-cart in Vuex source code as an example, open store/index.js, there is such a code:

export default new Vuex.Store({
  actions,
  getters,
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})
<div class="md-section-divider"></div>

Here are two sub-modules, cart and products. Let's open store/modules/cart.js and take a look at the state definition in cart module. The code is as follows:

  added: [],
  checkoutStatus: null
}
<div class="md-section-divider"></div>

We run this project, open the browser, and use the Vue debugging tool to see the status of Vuex, as shown in the following figure:

As you can see, under rootState, there are cart and products attributes respectively. key comes from the module name. value is the state defined in each module file, which mounts the module state onto rootState.

After we understand what the nested module state is all about, let's look back at three other important methods in the installModule process: registerMutation, registerAction, and wrapGetters. As the name implies, these three methods deal with mutations, actions and getters respectively. Let's first look at the definition of registerMutation:

registerMutation

function registerMutation (store, type, handler, path = []) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler(getNestedState(store.state, path), payload)
  })
}
<div class="md-section-divider"></div>

RegiserMutation is the initialization of store mutation. It takes four parameters. Store is the current Store instance, type is the key of mutation, handler is the callback function of mutation, path is the path of the current module. The function of mutation is to modify the state of the current module synchronously. The function first gets the corresponding array of mutation objects through type, and then push es a wrapper function of mutation into the array. This function receives a parameter payload, which is what we define. Additional parameters received at mutation time. When this function is executed, the callback function of mutation is called, and the state of the current module is obtained through the getNestedState(store.state, path) method, which is used as a parameter of the callback function together with playload. For instance:

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}
<div class="md-section-divider"></div>

Here we define a mutation, through the registerMutation method, we register the mutation, where the state corresponds to the current module state, n is the additional parameter payload, and then we will introduce from the perspective of source code analysis when the callback of the mutation is invoked and how the parameters are passed.

In Vuex, mutation is invoked through the API interface commit of the store instance. Let's look at the definition of commit function.

commit (type, payload, options) {
  // check object-style commit
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }
  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)
    })
  })
  if (!options || !options.silent) {
    this._subscribers.forEach(sub => sub(mutation, this.state))
  }
}
<div class="md-section-divider"></div>

Commit supports three parameters, type for mutation type, payload for additional parameters, options for configuration, such as silent, etc., which will be used later. The commit function first judges the type of the type, handles the case that the type is object, and then searches for the corresponding mutation according to the type. If it cannot find it, it outputs an error message. Otherwise, it traverses the array of mutation objects corresponding to the type and executes the handler(payload) method, which is wrapped Mutation Handler (handler) defined previously. All right, it's equivalent to execution. RegiserMutation registers the callback function and passes in the state of the current module and the additional parameter payload as parameters. Note that we still use the method of this._withCommit to submit mutation. At the end of the commit function, if it is not in silent mode, traverse this._subscribers, call the callback function, and Mutation and the current root state are passed in as parameters. So what is this._subscribers? The original Store instance of Vuex provides the subscribe API interface, which serves as a mutation for subscribe (registered listener) stores. Let's first look at its implementation:

subscribe (fn) {
  const subs = this._subscribers
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}
<div class="md-section-divider"></div>

The subscribe method is simple. The parameter he accepts is a callback function. It saves the callback function on this._subscribers and returns a function. When we call the returned function, we can deactivate the current function from listening to the mutation of the store. In fact, Vuex's built-in logger plug-in is based on subscribe interface to monitor the store's mutation, which we will describe in detail later.

registerAction

After understanding registerMutation, let's look at the definition of registerAction:

function registerAction (store, type, handler, path = []) {
const entry = store._actions[type] || (store._actions[type] = [])
  const { dispatch, commit } = store
  entry.push(function wrappedActionHandler (payload, cb) {
    let res = handler({
      dispatch,
      commit,
      getters: store.getters,
      state: getNestedState(store.state, path),
      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
    }
  })
 }
<div class="md-section-divider"></div>

RegiserAction is the initialization of store action, which is consistent with the parameters of registerMutation. It is different from mutation. mutation is to modify the state of the current module synchronously, while action can modify the state asynchronously. There is no misunderstanding. In the callback of action, the state will not be modified directly. It is still to modify the state by submitting a mutation. In Vuex, mutation is the only way to modify state. Let's see how action does that.

The function first gets the object array of the corresponding action through type, and then push es the wrapper function of an action into the array. This function receives two parameters, payload represents additional parameters, and cb represents callback function (actually we don't use it). When this function is executed, it calls the callback function of action and passes in a context object, which includes the commit and dispatch methods of the store, getter, state and rootState of the current module, and so on. Then the return value of the function is judged, if not a Promise. Object, then call Promise.resolve (res) to wrap res into a Promise object. This explains why the source code of Vuex depends on Promise, and the judgment of Promise is simple. Refer to the code src/util.js, the judgment of isPromise is as follows:

export function isPromise (val) {
  return val && typeof val.then === 'function'
}
<div class="md-section-divider"></div>

In fact, it's a simple method of checking the object's then, if it contains instructions, it's a Promise object.

Then judge store._devtoolHook, which can only be captured when Vuex devtools are turned on. The wrapper function of action finally returns res, which is an authentic Promise object. Let's take an example:

actions: {
  checkout ({ commit, state }, payload) {
    // Back up the current shopping cart
    const savedCartItems = [...state.cart.added]
    // Send checkout requests and happily empty shopping carts
    commit(types.CHECKOUT_REQUEST)
    // The shopping API receives a successful callback and a failed callback
    shop.buyProducts(
      products,
      // Successful operation
      () => commit(types.CHECKOUT_SUCCESS),
      // Failed operation
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}
<div class="md-section-divider"></div>

Here we define an action, and through the registerAction method we registered the action, where commit is the API interface of the store, through which we can submit a mutation in the action. State corresponds to the state of the current module, in which we can submit mutations synchronously or asynchronously. Next, we will introduce when the callback of this action is called and how the parameters are passed from the point of view of source code analysis.

It is necessary to know when the callback function of action is called. In Vuex, the call of action is called through the API interface dispatch of the store instance. Let's look at the definition of dispatch function.

dispatch (type, payload) {
  // check object-style dispatch
   if (isObject(type) && type.type) {
     payload = type
     type = type.type
   }
   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)
 }
<div class="md-section-divider"></div>

dispatch supports two parameters, type for action type and payload for additional parameters. The first few lines of code are very similar to the commit interface. They all find the array of action objects under the corresponding type. The only difference between them is the last part. It judges the array length of action objects. If the length is 1, it calls entry[0](payload) directly. This method is wrapped Action Handler (payload, which was defined earlier. Executing it is equivalent to executing the callback function registered by registerAction, and passing in the context of the current module and the additional parameter payload as parameters. So in the callback function of action, we can get the context of the current module, including the store commit and dispatch method, getter, the current module state and rootState, so action is very flexible.

wrapGetters

After understanding registerAction, let's look at the definition of wrapGetters:

function wrapGetters (store, moduleGetters, modulePath) {
  Object.keys(moduleGetters).forEach(getterKey => {
    const rawGetter = moduleGetters[getterKey]
    if (store._wrappedGetters[getterKey]) {
      console.error(`[vuex] duplicate getter key: ${getterKey}`)
      return
    }
    store._wrappedGetters[getterKey] = function wrappedGetter (store) {
      return rawGetter(
        getNestedState(store.state, modulePath), // local state
        store.getters, // getters
        store.state // root state
      )
    }
  })
}
<div class="md-section-divider"></div>

wrapGetters initialize the getters of the store, which accepts three parameters, store represents the current Store instance, module Getters represents all getters under the current module, and module Path corresponds to the path of the module. Careful students will find that unlike registerMutation and registerAction, the loop traversal of getters is placed in the function body, and one difference between getters and them is that getters are not allowed to duplicate the key s of getters.

What this function does is traverse moduleGetters, wrap each getter as a method, and add it to the store._wrappedGetters object. Note that getter key s are not allowed to repeat. In this wrapping method, the callback function of getter is executed, and the state of the current module, the getters of store and the rootState of store are taken as its parameters. Let's take an example:

export const cartProducts = state => {
  return state.cart.added.map(({ id, quantity }) => {
    const product = state.products.all.find(p => p.id === id)
    return {
      title: product.title,
      price: product.price,
      quantity
    }
  })
}
<div class="md-section-divider"></div>

Here we define a getter. We add this getter to the store._wrappedGetters object through the wrapGetters method just now. This corresponds to the parameter state of the callback function, which is the state of the current module. Next, we analyze how the function is called and how the parameters are passed from the source code point of view.

It is necessary to know when the callback function of getter is called. In Vuex, we know that when we can access the corresponding callback function of getter through this.$store. getters. xxxxgetters in the component, we need to bind the execution result of the wrapper function of the corresponding getter to `this.$store'. This part of the logic lies in In the resetStoreVM function. In the Store constructor, after the installModule method is executed, the resetStoreVM method is executed. Let's look at its definition:

resetStoreVM

function resetStoreVM (store, state) {
  const oldVm = store._vm
  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  Object.keys(wrappedGetters).forEach(key => {
    const fn = wrappedGetters[key]
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    })
  })
  // 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
  store._vm = new Vue({
    data: { state },
    computed
  })
  Vue.config.silent = silent
  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store)
  }
  if (oldVm) {
    // dispatch changes in all subscribed watchers
    // to force getter re-evaluation.
    store._withCommit(() => {
      oldVm.state = null
    })
    Vue.nextTick(() => oldVm.$destroy())
  }
}
<div class="md-section-divider"></div>

This method mainly resets a private _vm object, which is an instance of Vue. This _vm object retains our state tree and stores the getters of the store by calculating properties. Let's look at its implementation process in detail. We split this function into several parts to analyze:

 const oldVm = store._vm
  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  Object.keys(wrappedGetters).forEach(key => {
    const fn = wrappedGetters[key]
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    })
  })
<div class="md-section-divider"></div>

This part retains the existing store._vm object, and then traverses the store._wrappedGetters object. In the traversal process, the wrapper function of each getter is obtained in turn, and the results of the wrapper function execution are saved with the temporary variable of computer. Then we define the get method for store.getters by using the Object.defineProperty method of es5, which means that when we call this. $store.getters. xxxxgetters in the component, we access store._vm [xxxxgetters]. Let's go on to see:

// 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
 store._vm = new Vue({
   data: { state },
   computed
 })
 Vue.config.silent = silent
 // enable strict mode for new vm
 if (store.strict) {
   enableStrictMode(store)
 }
<div class="md-section-divider"></div>

This part of the code first takes the configuration of global Vue.config.silent, then temporarily sets this configuration to true, then instantiates an instance of Vue, passes in the state tree state of store as data, and passes in the temporary variable computed as calculation attribute. Then reset the previous silent configuration. The silent is set to true to cancel all logs and warnings for this_vm. The computed object is treated as the computed attribute of _vm, thus completing the registration of getters. Because when we access this.$store.getters.xxxgetters in a component, it's equivalent to accessing store._vm[xxxgetters], that is, accessing computed[xxxgetters], so you have access to the corresponding callback function of xxxgetters. This code finally judges the string attribute to decide whether to turn on strict mode or not. Let's see what strict mode does.

function enableStrictMode (store) {
  store._vm.$watch('state', () => {
    assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
  }, { deep: true, sync: true })
}
<div class="md-section-divider"></div>

What strict mode does is simply to monitor the changes in store._vm.state to see if the changes in state are changed by executing a mutation callback function. If the state is modified externally, the value of store._committing is false, which throws an error. Again, in Vuex The state modification can only be made in the mutation callback function.

Back to the resetStoreVM function, let's take a look at the last part:

if (oldVm) {
  // dispatch changes in all subscribed watchers
  // to force getter re-evaluation.
  store._withCommit(() => {
    oldVm.state = null
  })
  Vue.nextTick(() => oldVm.$destroy())
}
<div class="md-section-divider"></div>

The logic here is simple, because each time this function creates a new Vue instance and assigns it to store._vm, the state of the old_vm object is set to null and the $destroy method is called to destroy the old_vm object.

So at this point, the initialization of Vuex is basically over. The core of initialization is installModule and
resetStoreVM function. By registering mutations, actions and getters, we know that state is divided into modules, and a state tree is formed by nesting of modules. While actions, mutations and getters are global, the keys of actions and mutations allow duplication, but the keys of getters do not allow duplication. It is officially recommended that we add a namespace to these global objects when they are defined to avoid naming conflicts.
After introducing the initial method of Vuex from the point of view of source code, we will analyze the source code from the direction of API provided by Vuex and see how these APIs are implemented.

4. Vuex API analysis

Vuex common APIs such as dispatch, commit, subscribe have been introduced before, we will not repeat them here. The following APIs of Store, although not often used, are also good to understand.

watch(getter, cb, options)

The watch function is to responsively monitor the return value of a getter method and call the callback when the value changes. Getter receives store state as the only parameter. Take a look at its implementation:

watch (getter, cb, options) {
    assert(typeof getter === 'function', `store.watch only accepts a function.`)
    return this._watcherVM.$watch(() => getter(this.state), cb, options)
  }
<div class="md-section-divider"></div>

The function first asserts that the getter of the watch must be a method, and then uses the $watch method of an internal Vue instance object `this. _watcher VM'to observe the change of the return value of the getter method. If there is a change, the cb function is called, and the parameters of the callback function are the new value and the old value. The watch method returns a method and calls it to cancel the observation.

registerModule(path, module)

The function of registerModule is to register a dynamic module. Sometimes when we load some business asynchronously, we can register the module dynamically through this API interface to see its implementation.

registerModule (path, module) {
    if (typeof path === 'string') path = [path]
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    this._runtimeModules[path.join('.')] = module
    installModule(this, this.state, path, module)
    // reset store to update getters...
    resetStoreVM(this, this.state)
  }
<div class="md-section-divider"></div>

The function first judges the path, and converts the path to an Array if the path is a string. The module object is then cached into this._runtimeModules, where path uses point connections as the key of the object. Then, as with the logic for initializing the Store, the installModule and resetStoreVm methods are called to install the dynamically injected module once.

unregisterModule(path)

Relative to the registerModule method is the unregisterModule method. Its function is to cancel a dynamic module. Let's take a look at its implementation.

unregisterModule (path) {
    if (typeof path === 'string') path = [path]
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    delete this._runtimeModules[path.join('.')]
    this._withCommit(() => {
      const parentState = getNestedState(this.state, path.slice(0, -1))
      Vue.delete(parentState, path[path.length - 1])
    })
    resetStore(this)
  }
<div class="md-section-divider"></div>

The function first determines the type of path, which is the same logic as registration. Next, delete the module corresponding to the key connected by the path point from this._runtimeModules. The state object of the current module is then deleted from the parent state by this._withCommit method. Finally, call resetStore(this) Method, take a look at the definition of this method:

function resetStore (store) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  const state = store.state
  // init root module
  installModule(store, state, [], store._options, true)
  // init all runtime modules
  Object.keys(store._runtimeModules).forEach(key => {
    installModule(store, state, key.split('.'), store._runtimeModules[key], true)
  })
  // reset vm
  resetStoreVM(store, state)
}
<div class="md-section-divider"></div>

The purpose of this method is to reset the store object, reset the store's _actions, _mutations, _wrappedGetters, and so on. Then call installModules again to reinstall the properties corresponding to Module. Note that our last parameter, hot, here is true, indicating that it is a hot update. So in the installModule method body class, the following logic will not execute

function installModule (store, rootState, path, module, hot) {
  ... 
  // 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, state || {})
    })
  }
  ...
}
<div class="md-section-divider"></div>

Since hot is always true, we will not reset the state tree here, and our state will remain unchanged. Because we have explicitly deleted the state under the corresponding path, all we need to do is to re-register muations, actions and getters.

Callback the resetStore method, and then iterate through the. _runtimeModules module to reinstall all remaining runtime Moudles. Finally, the resetStoreVM method is called to reset the Store's _vm object.

hotUpdate(newOptions)

The function of hotUpdate is to hot load new action s and mutation s. Take a look at its implementation:

hotUpdate (newOptions) {
  updateModule(this._options, newOptions)
  resetStore(this)
}
<div class="md-section-divider"></div>

The function first calls the updateModule method to update the status, where the current Store's opinion configuration and the newOptions to be updated are taken as parameters. Let's look at the implementation of this function:

function updateModule (targetModule, newModule) {
  if (newModule.actions) {
    targetModule.actions = newModule.actions
  }
  if (newModule.mutations) {
    targetModule.mutations = newModule.mutations
  }
  if (newModule.getters) {
    targetModule.getters = newModule.getters
  }
  if (newModule.modules) {
    for (const key in newModule.modules) {
      if (!(targetModule.modules && targetModule.modules[key])) {
        console.warn(
          `[vuex] trying to add a new module '${key}' on hot reloading, ` +
          'manual reload is needed'
        )
        return
      }
      updateModule(targetModule.modules[key], newModule.modules[key])
    }
  }
}
<div class="md-section-divider"></div>

First, we judge the actions, mutations, and getters of the newOptions object. If there are these attributes, we replace the attributes corresponding to the targetModule (options of the current Store). Finally, if the new Options contains the module key, it traverses the module object, and if the key corresponding to the modules is not in the previous modules, a warning is issued, because this is the addition of a new module, which requires manual reloading. If the key is in the previous modules, the updateModule is called recursively, and the sub-module is hot updated.

After calling the updateModule, go back to the hotUpdate function, and then call the resetStore method to reset the store, as we have just described.

replaceState

The purpose of replaceState is to replace the entire rootState, which is generally used for debugging. Let's see its implementation:

replaceState (state) {
    this._withCommit(() => {
      this._vm.state = state
    })
  }
<div class="md-section-divider"></div>

The function is very simple. It calls this._withCommit method to modify the rootState of Store. The API is provided because we can't change the state outside of the muations callback function.

So far, the API part has been introduced, in fact, the entire Vuex source code under the src/index.js file code has basically gone through.

5. 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 sugars 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 a locally computed property. 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
    }
  })
}
<div class="md-section-divider"></div>

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'
])
<div class="md-section-divider"></div>

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
}
<div class="md-section-divider"></div>

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] }))
}
<div class="md-section-divider"></div>

This method determines whether the parameter map is an array, and 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 (in terms of the usage 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. Convert each key of the array to a {key, val: key} object. 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. $store. state [val] is taken as the return value directly. The return value of mappedState.

So why is the return value of the mapState function such an object, because the function of mapState is to map the global state and getters to the computed computational properties of the current component, and we know that every computational attribute in 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
    }
  })
}
<div class="md-section-divider"></div>

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
    }
  }
}
<div class="md-section-divider"></div>

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'
])
<div class="md-section-divider"></div>

The result after calling the mapState function is as follows:

computed: {
  count() {
    return this.$store.state['count']
  }
}
<div class="md-section-divider"></div>

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
}
<div class="md-section-divider"></div>

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: {
    // Mixing getter into computer using object extension operators
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}
<div class="md-section-divider"></div>

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']
    }
  }
}
<div class="md-section-divider"></div>

Let's look at another example where the parameter mapGetters is an object:

computed: mapGetters({
  // Map this.doneCount to store.getters.doneTodosCount
  doneCount: 'doneTodosCount'
})
<div class="md-section-divider"></div>

The result after calling the mapGetters function is as follows:

computed: {
  doneCount() {
    return this.$store.getters['doneTodosCount']
  }
}
<div class="md-section-divider"></div>

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
}
<div class="md-section-divider"></div>

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')
    })
  }
}
<div class="md-section-divider"></div>

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))
    }
  }
}
<div class="md-section-divider"></div>

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
}
<div class="md-section-divider"></div>

The implementation of functions is almost the same as that of mapActions. The only difference is that the common method of store 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')
    })
  }
}
<div class="md-section-divider"></div>

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))
    }
  }
}
<div class="md-section-divider"></div>

6. plug-in

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))
<div class="md-section-divider"></div>

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()] : []
})
<div class="md-section-divider"></div>

In the two examples above, 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. 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)
  })
}
<div class="md-section-divider"></div>

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 _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, through devtoolHook.on ('vuex: travel-to-state', targetState =>){
store.replaceState(targetState)
}) To monitor the traval-to-state events of Vuex and replace the current state tree with the target state tree, this function also uses the Vue developer tool to replace the state of Vuex.

Finally, store. subscribe ((mutation, state) => is used.{
devtoolHook.emit('vuex:mutation', mutation, state)
}) Method Subscribe to changes in store state. When the store mutation submits changes in state, it triggers a callback function - dispatch a Vuex mutation event through devtoolHook, mutation and rootState as parameters, so that the developer tool can observe the real-time changes of Vuex state and display the latest state tree 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
}
<div class="md-section-divider"></div>

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 find (cache, C = > C. original) is passed in the deepCopy process === When a circular reference is found, the corresponding copy in the cache is returned directly, thus avoiding the case of infinite loops.

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, we first get a copy of the current state through the deepCopy method and save it with the nextState variable. Next, get the string whose mutation has been formatted at the current formatting time, and then output prevState, mutation, and using console.group and console.log groupings NextState, where 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.

Four, summary

The source code analysis of Vuex 2.0 comes to a close. Finally, I will share with you the caution of the source code: before studying the source code of a library or framework, first of all, its usage scenario, official website documents, etc. Then, we must use it, at least write a few demo s to achieve a proficient level; finally, we can understand it from multiple dimensions such as entrance, API, usage method and so on. Its internal implementation details. If the library is too large, it should be separated into modules and functions and digested bit by bit.

Finally, there is another question, some students will ask, the source code is so boring, we analyze the benefits of learning it? First, learning the source code will help us to master and apply the library or framework in depth; secondly, we can learn many programming skills in the source code, which can be migrated to our usual development work; finally, for some senior development engineers, we can learn its design ideas, and one day we will also design a library or framework. It's very helpful. It's also a very good way to improve your abilities.

Keywords: Vue Attribute github npm

Added by dcf1023 on Tue, 21 May 2019 02:43:01 +0300