Vuex source code analysis

write in front

Because I am very interested in Vue.js, and the technology stack I usually work on is also Vue.js, I spent some time studying and studying the Vue.js source code in the past few months, and made a summary and output.

The original address of the article: https://github.com/answershuto/learnVue .

During the learning process, Chinese annotations were added to Vue https://github.com/answershuto/learnVue/tree/master/vue-src and Vuex annotations https://github.com/answershuto/learnVue/tree/ master/vuex-src , I hope it can be helpful to other friends who want to learn the source code.

There may be some deviations in understanding. You are welcome to raise issues and point out, learn together and make progress together.

Vuex

When we use Vue.js to develop complex applications, we often encounter multiple components sharing the same state, or multiple components will update the same state. When the amount of application code is small, we can Communication to maintain and modify data, or to transfer and modify data through the event bus. However, when the application becomes larger, the code will become difficult to maintain. The multi-level nested data passed from the parent component through the prop is extremely fragile due to the deep level, and the event bus will also be due to the increase of components and the amount of code. The interaction becomes complicated and it is difficult to clarify the transmission relationship among them.

So why can't we separate the data layer from the component layer? Putting the data layer globally forms a single Store, and the component layer becomes thinner, dedicated to data display and operation. All data changes need to go through the global store to form a one-way data flow, making data changes "predictable".

Vuex is a library specially designed for the Vue.js framework for state management of Vue.js applications. It draws on the basic ideas of Flux and redux, extracts the shared data to the global, and stores it in a single instance. , while using the responsive mechanism of Vue.js for efficient state management and updating. It is precisely because Vuex uses the "responsive mechanism" inside Vue.js that Vuex is a framework specially designed for and highly compatible with Vue.js (the advantage is that it is more concise and efficient, the disadvantage is that it can only be matched with Vue.js use). For specific usage and API, please refer to Vuex's official website .

Let's take a look at the data flow diagram of this Vuex first. Students who are familiar with the use of Vuex should already know something.

Vuex implements a one-way data flow, and has a State to store data globally. All operations to modify the State must be performed through Mutation. Mutation also provides a subscriber mode for external plug-ins to call to obtain State data updates. All asynchronous interfaces need to take Action, which is commonly used to call the back-end interface to asynchronously obtain updated data, and Action cannot directly modify the State, and it is still necessary to modify the data of the State through Mutation. Finally, according to the change of State, it is rendered to the view. Vuex operation relies on Vue's internal data two-way binding mechanism, and requires a new Vue object to achieve "responsiveness", so Vuex is a state management library specially designed for Vue.js.

Install

Friends who have used Vuex must know that the installation of Vuex is very simple, you only need to provide a store, and then execute the following two lines of code to complete the introduction of Vuex.

Vue.use(Vuex);

/*将store放入Vue创建时的option中*/
new Vue({
    el: '#app',
    store
});

So the question is, how does Vuex inject the store into the Vue instance?

Vue.js provides the Vue.use method to install plugins for Vue.js. Internally, the plugin is installed by calling the plugin's install method (when the plugin is an object).

Let's take a look at the install implementation of Vuex.

/*暴露给外部的插件install方法,供Vue.use调用安装插件*/
export function install (_Vue) {
  if (Vue) {
    /*避免重复安装(Vue.use内部也会检测一次是否重复安装同一个插件)*/
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  /*保存Vue,同时用于检测是否重复安装*/
  Vue = _Vue
  /*将vuexInit混淆进Vue的beforeCreate(Vue2.0)或_init方法(Vue1.0)*/
  applyMixin(Vue)
}

This install code does two things, one is to prevent Vuex from being installed repeatedly, and the other is to execute applyMixin to initialize Vuex by executing the vuexInit method. Vuex handles Vue1.0 and 2.0 differently. If it is Vue1.0, Vuex will put the vuexInit method into Vue's _init method, while for Vue2.0, it will confuse vuexinit into Vue's beforeCreacte hook middle. Let's take a look at the code of vuexInit.

 /*Vuex的init钩子,会存入每一个Vue实例等钩子列表*/
  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      /*存在store其实代表的就是Root节点,直接执行store(function时)或者使用store(非function)*/
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      /*子组件直接从父组件中获取$store,这样就保证了所有组件都公用了全局的同一份store*/
      this.$store = options.parent.$store
    }
  }

vuexInit will try to get the store from options. If the current component is the root component (Root node), there will be a store in options, and you can directly get it and assign it to $store. If the current component is not the root component, get the $store reference of the parent component through parent in options. In this way, all components get the Store instance of the same memory address, so we can happily access the global Store instance through this.$store in each component.

So, what is a Store instance?

Store

We pass in the root component to the store, which is the Store instance, which is constructed with the Store method provided by Vuex.

export default new Vuex.Store({
    strict: true,
    modules: {
        moduleA,
        moduleB
    }
});

Let's take a look at the implementation of the Store. First is the constructor.

constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    /*
      在浏览器环境下,如果插件还未安装(!Vue即判断是否未安装),则它会自动安装。
      它允许用户在某些情况下避免自动安装。
    */
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    if (process.env.NODE_ENV !== 'production') {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `Store must be called with the new operator.`)
    }

    const {
      /*一个数组,包含应用在 store 上的插件方法。这些插件直接接收 store 作为唯一参数,可以监听 mutation(用于外部地数据持久化、记录或调试)或者提交 mutation (用于内部数据,例如 websocket 或 某些观察者)*/
      plugins = [],
      /*使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。*/
      strict = false
    } = options

    /*从option中取出state,如果state是function则执行,最终得到一个对象*/
    let {
      state = {}
    } = options
    if (typeof state === 'function') {
      state = state()
    }

    // store internal state
    /* 用来判断严格模式下是否是用mutation修改state的 */
    this._committing = false
    /* 存放action */
    this._actions = Object.create(null)
    /* 存放mutation */
    this._mutations = Object.create(null)
    /* 存放getter */
    this._wrappedGetters = Object.create(null)
    /* module收集器 */
    this._modules = new ModuleCollection(options)
    /* 根据namespace存放module */
    this._modulesNamespaceMap = Object.create(null)
    /* 存放订阅者 */
    this._subscribers = []
    /* 用以实现Watch的Vue实例 */
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    /*将dispatch与commit调用的this绑定为store对象本身,否则在组件内部this.dispatch时的this会指向组件的vm*/
    const store = this
    const { dispatch, commit } = this
    /* 为dispatch与commit绑定this(Store实例本身) */
    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
    /*严格模式(使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误)*/
    this.strict = strict

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    /*初始化根module,这也同时递归注册了所有子modle,收集所有module的getter到_wrappedGetters中去,this._modules.root代表根module才独有保存的Module对象*/
    installModule(this, state, [], this._modules.root)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    /* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
    resetStoreVM(this, state)

    // apply plugins
    /* 调用插件 */
    plugins.forEach(plugin => plugin(this))

    /* devtool插件 */
    if (Vue.config.devtools) {
      devtoolPlugin(this)
    }
  }

In addition to initializing some internal variables, the construction class of the Store mainly executes installModule (initializing the module) and resetStoreVM (making the store "responsive" through the VM).

installModule

The function of installModule is mainly to register mutation, action and getter after adding namespace namespace (if any) to module, and install all submodules recursively.

/*初始化module*/
function installModule (store, rootState, path, module, hot) {
  /* 是否是根module */
  const isRoot = !path.length
  /* 获取module的namespace */
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  /* 如果有namespace则在_modulesNamespaceMap中注册 */
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    /* 获取父级的state */
    const parentState = getNestedState(rootState, path.slice(0, -1))
    /* module的name */
    const moduleName = path[path.length - 1]
    store.`_withCommit`(() => {
      /* 将子module设置称响应式的 */
      Vue.set(parentState, moduleName, module.state)
    })
  }

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

  /* 遍历注册mutation */
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  /* 遍历注册action */
  module.forEachAction((action, key) => {
    const namespacedType = namespace + key
    registerAction(store, namespacedType, action, local)
  })

  /* 遍历注册getter */
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  /* 递归安装mudule */
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

resetStoreVM

Before talking about resetStoreVM, let's take a look at a small demo.

let globalData = {
    d: 'hello world'
};
new Vue({
    data () {
        return {
            $$state: {
                globalData
            }
        }
    }
});

/* modify */
setTimeout(() => {
    globalData.d = 'hi~';
}, 1000);

Vue.prototype.globalData = globalData;

/* 任意模板中 */
<div>{{globalData.d}}</div>

The above code has a globalData in the global, which is passed into the data of a Vue object, and then displays the variable in any Vue template. Because globalData is already on the prototype of Vue at this time, it is directly accessed through this.prototype. That is, {{prototype.d}} in the template. At this point, setTimeout modifies globalData.d after 1s, and we find that globalData.d in the template has changed. In fact, the above part is that Vuex relies on Vue core to realize the "responsiveness" of data.

Students who are not familiar with the responsive principle of Vue.js can learn how Vue.js performs two-way data binding through the author's other article , the principle of responsiveness.

Then look at the code.

/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
function resetStoreVM (store, state, hot) {
  /* 存放之前的vm对象 */
  const oldVm = store._vm 

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

  /* 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的computed属性 */
  forEachValue(wrappedGetters, (fn, 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 has added
  // some funky global mixins
  const silent = Vue.config.silent
  /* Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告 */
  Vue.config.silent = true
  /*  这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  /* 使能严格模式,保证修改store只能通过mutation */
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    /* 解除旧vm的state的引用,以及销毁旧的Vue对象 */
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

resetStoreVM will first traverse the wrappedGetters, and use the Object.defineProperty method to bind the get method to each getter, so that we can access this.$store.getter.test in the component is equivalent to accessing store._vm.test.

forEachValue(wrappedGetters, (fn, 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
  })
})

After that, Vuex adopted a new Vue object to realize the "responsiveness" of data, and used the data two-way binding function provided by Vue.js to realize the synchronous update of store data and view.

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

At this time, when we access store._vm.test, we also access the properties in the Vue instance.

After these two steps are completed, we can access the test property in vm through this.$store.getter.test.

strict mode

The option of Vuex's Store constructor class has a strict parameter, which can control Vuex to execute strict mode. In strict mode, all operations that modify state must be implemented through mutation, otherwise an error will be thrown.

/* 使能严格模式 */
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      /* 检测store中的_committing的值,如果是true代表不是通过mutation的方法修改的 */
      assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

First of all, in strict mode, Vuex will use the $watch method of vm to observe the $$state, which is the state of the Store, and enter the callback when it is modified. We found that there is only one sentence in the callback. The assert assertion is used to detect store._committing. When store._committing is false, the assertion will be triggered and an exception will be thrown.

We found that in the commit method of the Store, the statement that executes the mutation is like this.

this._withCommit(() => {
  entry.forEach(function commitIterator (handler) {
    handler(payload)
  })
})

Let's take a look at the implementation of _withCommit.

_withCommit (fn) {
  /* 调用withCommit修改state的值时会将store的committing值置为true,内部会有断言检查该值,在严格模式下只允许使用mutation来修改store中的值,而不允许直接修改store的数值 */
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}

We found that when the state data is modified through commit (mutation), the committing will be set to true before calling the mutation method, and then the data in the state will be modified through the mutation function. At this time, the callback in $watch is triggered to assert whether the committing is not An exception will be thrown (committing is true at this time). When we directly modify the data of the state, the callback of $watch is triggered to execute the assertion. At this time, if the committing is false, an exception will be thrown. This is the implementation of Vuex's strict mode.

Next, let's take a look at some of the APIs provided by the Store.

commit(mutation

/* 调用mutation的commit方法 */
commit (_type, _payload, _options) {
  // check object-style commit
  /* 校验参数 */
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  /* 取出type对应的mutation的方法 */
  const entry = this._mutations[type]
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }
  /* 执行mutation中的所有方法 */
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })
  /* 通知所有订阅者 */
  this._subscribers.forEach(sub => sub(mutation, this.state))

  if (
    process.env.NODE_ENV !== 'production' &&
    options && options.silent
  ) {
    console.warn(
      `[vuex] mutation type: ${type}. Silent option has been removed. ` +
      'Use the filter functionality in the vue-devtools'
    )
  }
}

The commit method will find and call the mutation methods corresponding to all types in _mutations according to the type, so when there is no namespace, the commit method will trigger the mutation methods in all modules. After executing all mutations, all subscribers in _subscribers will be executed. Let's see what _subscribers is.

Store provides a subscribe method to the outside world to register a subscription function, push it to _subscribers of the Store instance, and return a method to unregister the subscriber from _subscribers.

/* 注册一个订阅函数,返回取消订阅的函数 */
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)
    }
  }
}

After the commit is over, the subscribers in these _subscribers will be called. This subscriber mode provides an external possibility to monitor state changes. When the state changes through mutation, these changes can be effectively compensated.

dispatch(action

Let's take a look at the implementation of dispatch.

/* 调用action的dispatch方法 */
dispatch (_type, _payload) {
  // check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)

  /* actions中取出type对应的ation */
  const entry = this._actions[type]
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown action type: ${type}`)
    }
    return
  }

  /* 是数组则包装Promise形成一个新的Promise,只有一个则直接返回第0个 */
  return entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)
}

And what to do when registerAction.

/* 遍历注册action */
function registerAction (store, type, handler, local) {
  /* 取出type对应的action */
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload, cb) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    /* 判断是否是Promise */
    if (!isPromise(res)) {
      /* 不是Promise对象的时候转化称Promise对象 */
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      /* 存在devtool插件的时候触发vuex的error给devtool */
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

Because the action of push into _actions is encapsulated (wrappedActionHandler) when registerAction, so we get state, commit and other methods in the first parameter of dispatch. After that, the execution result res will be judged whether it is a Promise or not, it will be encapsulated and converted into a Promise object. When dispatching, it is taken from _actions, and when there is only one, it is returned directly, otherwise it is processed with Promise.all and then returned.

watch

/* 观察一个getter方法 */
watch (getter, cb, options) {
  if (process.env.NODE_ENV !== 'production') {
    assert(typeof getter === 'function', `store.watch only accepts a function.`)
  }
  return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}

Friends who are familiar with Vue should be familiar with the watch method. A more ingenious design is adopted here. _watcherVM is an instance of Vue, so watch can directly use the watch feature inside Vue to provide a way to observe the changes of the data getter.

registerModule

/* 注册一个动态module,当业务进行异步加载的时候,可以通过该接口进行注册动态module */
registerModule (path, rawModule) {
  /* 转化称Array */
  if (typeof path === 'string') path = [path]

  if (process.env.NODE_ENV !== 'production') {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    assert(path.length > 0, 'cannot register the root module by using registerModule.')
  }

  /*注册*/
  this._modules.register(path, rawModule)
  /*初始化module*/
  installModule(this, this.state, path, this._modules.get(path))
  // reset store to update getters...
  /* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
  resetStoreVM(this, this.state)
}

registerModule is used to register a dynamic module, that is, use this interface when registering the module after the store is created. The internal implementation actually only has two steps: installModule and resetStoreVM, which have been mentioned before, and will not be repeated here.

unregisterModule

 /* 注销一个动态module */
unregisterModule (path) {
  /* 转化称Array */
  if (typeof path === 'string') path = [path]

  if (process.env.NODE_ENV !== 'production') {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
  }

  /*注销*/
  this._modules.unregister(path)
  this._withCommit(() => {
    /* 获取父级的state */
    const parentState = getNestedState(this.state, path.slice(0, -1))
    /* 从父级中删除 */
    Vue.delete(parentState, path[path.length - 1])
  })
  /* 重制store */
  resetStore(this)
}

Similarly, the method unregisterModule corresponding to registerModule dynamically unregisters the module. The way to do this is to first remove the module from the state, and then use resetStore to recreate the store.

resetStore

/* 重制store */
function resetStore (store, hot) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  store._modulesNamespaceMap = Object.create(null)
  const state = store.state
  // init all modules
  installModule(store, state, [], store._modules.root, true)
  // reset vm
  resetStoreVM(store, state, hot)
}

The resetStore here actually means that after initializing the _actions in the store, re-execute the installModule and resetStoreVM to initialize the module and use the Vue feature to make it "responsive", which is consistent with the constructor.

plugin

Vue provides a very useful plugin Vue.js devtools

/* 从window对象的__VUE_DEVTOOLS_GLOBAL_HOOK__中获取devtool插件 */
const devtoolHook =
  typeof window !== 'undefined' &&
  window.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
  if (!devtoolHook) return

  /* devtoll插件实例存储在store的_devtoolHook上 */
  store._devtoolHook = devtoolHook

  /* 出发vuex的初始化事件,并将store的引用地址传给deltool插件,使插件获取store的实例 */
  devtoolHook.emit('vuex:init', store)

  /* 监听travel-to-state事件 */
  devtoolHook.on('vuex:travel-to-state', targetState => {
    /* 重制state */
    store.replaceState(targetState)
  })

  /* 订阅store的变化 */
  store.subscribe((mutation, state) => {
    devtoolHook.emit('vuex:mutation', mutation, state)
  })
}

If the plugin is already installed, a __VUE_DEVTOOLS_GLOBAL_HOOK__ is exposed on the windows object. When devtoolHook is used, it will trigger the "vuex:init" event to notify the plugin, and then listen to the "vuex:travel-to-state" event through the on method to reset the state. Finally, a subscriber is added through the Store's subscribe method. After triggering the commit method to modify the mutation data, the subscriber will be notified, thereby triggering the "vuex:mutation" event.

finally

Vuex is a very good library with a small amount of code and a clear structure, which is very suitable for studying and learning its internal implementation. The recent series of source code readings have also benefited me a lot. Writing this article also hopes to help more students who want to learn and explore the internal implementation principles of Vuex.

about

Author: Ran Mo

Email:[email protected] or [email protected]

Github: https://github.com/answershuto

Blog:http://answershuto.github.io/

Zhihu homepage: https://www.zhihu.com/people/cao-yang-49/activities

Know the column: https://zhuanlan.zhihu.com/ranmo

Nuggets: https://juejin.im/user/58f87ae844d9040069ca7507

osChina:https://my.oschina.net/u/3161824/blog

Please indicate the source when reprinting, thank you.

Welcome to pay attention to my public number

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324402978&siteId=291194637