preface

As an official vUE state management framework, VUEX, with its simple API design and convenient development tool support, has been well applied in medium and large VUE projects. As a late comer of Flux architecture, it absorbs various advantages of its predecessor Redux and perfectly combines the responsive data of Vue. Personally, I think the development experience has surpassed the gay friends of React + Redux.

In the months since the project started the development of VUE, I have become more and more curious about the principle of Vuex. Today, I will summarize what I have learned in these days into a paper, hoping to help children who are curious about Vuex.

Understand the computed

The use of store data in VUex is essentially dependent on computed, a commonly used attribute in VUE. The simplest official example is as follows

Var vm = new Vue({el: '#example', data: {message: 'Hello'}, computed: {// Calculate attributes of the getter reversedMessage: Function () {// 'this' points to the vm instance return this.message.split('').reverse().join()}}})Copy the code

Have you ever wondered how computed data in Vue is updated, and why, when the VM. Message changes, the VM. ReversedMessage changes automatically?

Let’s take a look at the source code for the Data attribute and computed in VUE.

/ / SRC/core/instance/state. The js / / initializes the component state export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, Opts.methods) // If (opts.data) {initData(vm)} else {observe(vm._data = {}, True /* asRootData */)} // If (opts.computed) initComputed(VM, opts.computed) if (opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

Trigger automatically when the component instantiation initState method, this method is mainly completed the initialization data, the methods, props, computed, watch these we commonly used attributes, we come to see if we need to focus on initData and initComputed (to save time, Remove less relevant code)

Look at the initData line first

// src/core/instance/state.js function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} // ..... // Pass vue's data to the observe method observe(data, true /* asRootData */) } // src/core/observer/index.js export function observe (value: any, asRootData: ? boolean): Observer | void { if (! isObject(value)) { return } let ob: Observer | void // ... Ob = new Observer(value) if (asRootData && ob) {ob.vmcount+ +} return ob}Copy the code

On initialization, the Observe method essentially instantiates an Observer whose class looks like this

// src/core/observer/index.js export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: Def (value, '__ob__', this) {this.value = value //... def(value, '__ob__', this) {this.value = value //... This.walk (value)} walk(obj: Object) {const keys = object.keys (obj) for (let I = 0; i < keys.length; DefineReactive defineReactive(obj, keys[I], obj[keys[I]])}}}Copy the code

In the constructor of the object, the walk method is finally called, which iterates through all the attributes in data and calls defineReactive, which is the basis of the MDV(Model-driven View) implemented by VUE. In essence, it is the proxy of the data set,get method, when the data is modified or obtained, can sense (of course, vUE also consider array, Object nested Object and other situations, this article is not analyzed). Let’s look specifically at define Active’s source code

// src/core/observer/index.js export function defineReactive ( obj: Object, key: string, val: any, customSetter? :? Function, shallow? : Boolean) {// Important, when calling this method for a specific attribute, Const dep = new dep () // This method returns a description of an attribute in the object // API address https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor Const property = Object. GetOwnPropertyDescriptor (obj, key) / / if the description is not change, direct return, because we can not change, so won't be able to set and get methods, If (property && property.64x === false) {return} // Cater for pre-defined getter/setters const getter  = property && property.get const setter = property && property.set let childOb = ! Shallow && Observe (val) // Redefine attributes in data to proxy get and set. Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : Val // Collect dependencies, ReversedMessage if (dep.target) {dep.depend() if (childOb) {childob.dep.depend ()} if (Array.isArray(value)) { dependArray(value) } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal ! == newVal && value ! == value)) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = ! Shallow && observe(newVal) // Notify dependency to update dep.notify()}})}Copy the code

As we can see, the dep.depend() method is called when dep.target is present in the get method of the proxy property. This method is very simple, but before we get to it, we need to recognize a new class dep

Dep is a dependency processing object implemented by VUE. It acts as a link between Reactive Data and Watcher. The code is very simple

// src/core/observer/dep.js export default class Dep { static target: ? Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { const subs = this.subs.slice() for (let i =  0, l = subs.length; i < l; I++) {// update the value of watcher, similar to watcher.evaluate(), Subs [I].update()}}} // When the value of the computed property is first computed, Dep. Target = null const targetStack = [] export function pushTarget (_target: Watcher) {// During a dependency collection, if another dependency collection task is started (e.g. For current computed attributes nested with other computed attributes, // The current target will be temporarily stored in the targetStack for dependency collection of other targets, If (dep.target) targetstack.push (dep.target) dep.target = _target} export function popTarget () {// When the nested dependency collection task is completed, Dep.target = targetstack.pop ()} Restore target to Watcher and continue with dependency collection.Copy the code

The code is very simple. Back to calling dep.depend(), which is called when dep.target exists and which adds the deP to Watcher’s newDeps, Insert subs in the DEP object accessing the current property into the watcher of the current DEP.target. It’s a little convoluted, but that’s okay, we’ll follow the example in a minute and it’ll make sense.

Dep.notify () is called when we set the value of a property in data. Dep.notify () is called when we set the value of a property in data. The notify method adds all watcher updates to the DEP. That is, when you change a property value in data, dep.notify() is called at the same time to update all watchers that depend on that value.

With initData out of the way, let’s move on to initComputed, which primarily addresses the problem of when to set dep.target (if it’s not set, dep.Depend () won’t be called, so you can’t get dependencies).

// src/core/instance/state.js const computedWatcherOptions = { lazy: true } function initComputed (vm: Component, computed: Const watchers = vm._computedWatchers = object. create(null) const isSSR = isServerRendering()  for (const key in computed) { const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get if (! IsSSR) {// All attributes to generate your own watcher, can be in this. See _computedWatchers watchers [key] = new watcher (vm, getter | | it, it, computedWatcherOptions ) } if (! {// Focus 2 defineComputed(vm, key, userDef)}}}Copy the code

There are two areas of concern when initializing computed

  1. Generates its own Watcher instance for each property and passes {lazy: true} as options
  2. The defineComputed method is called for each attribute (essentially like Data, proxying its own set and GET methods, we’ll focus on the proxy get method)

Let’s look at Watcher’s constructor

// src/core/observer/watcher.js constructor ( vm: Component, expOrFn: string | Function, cb: Function, options? : Object ) { this.vm = vm vm._watchers.push(this) if (options) { this.deep = !! options.deep this.user = !! options.user this.lazy = !! options.lazy this.sync = !! options.sync } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for Batching this.active =true this.dirty = this.lazy // If you initialize lazy=true (implying a computed attribute), This.deps = [] this.newdeps = [] this.depids = new Set() this.newDepids = new Set() this.getter = ExpOrFn // In computed instantiation, place specific attribute values in this.getter // omit irrelevant code this.value = this.lazy? Undefined: this.get()}Copy the code

In addition to routine initialization, there are two important lines of code

this.dirty = this.lazy this.getter = expOrFn

In computed generated Watcher, the watcher lazy is set to true to reduce the amount of computation. Thus, this.dirty is also true when instantiated, indicating that data needs to be updated. Remember that in computed initializations now, the watcher generated for each attribute is set to true for both dirty and lazy. At the same time, computed attribute values (generally funtion) are stored in watcher’s getter.

Let’s look at the second concern, defineComputed, what is the get method of the property propped up

// src/core/instance/state.js function createComputedGetter (key) { return function computedGetter () { const watcher = This._computedWatchers && this._computedWatchers[key] // If watcher if (watcher) {// When initialized, the dirty is true, that is, When a property in computed is first accessed, the watcher.evaluate() method is called; if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } }Copy the code

When values in computed are accessed for the first time, the evalute() method is called because watcher.dirty = watcher.lazy is initialized, The evalute() method is simple and simply calls the Get method in the Watcher instance and sets dirty = false, which we put together

/ / SRC/core/instance/state. The js the evaluate () {this. Value = this. The get () this. Dirty = false} the get () {/ / the point 1, PushTarget (this) let value const vm = this.vm try {// Key 2, what is triggered when a method passed by the user is called? Value = this.getter.call(vm, vm)} Catch (e) {} finally {popTarget()} return value}Copy the code

In the get method, the first line calls pushTarget, All it does is set the dep. target to the watcher passed in, that is, the watcher of the attributes in computed that are accessed, and then call the value = this.gett. call(vm, vm) method. Think about what happens when you call this method.

This. Getter, as mentioned in the Watcher builder, is essentially a method passed in by the user, that is, this. Getter. Call (VM, VM) will call the method declared by the user, So if the method uses a value from this.data or some other object wrapped with defineReactive, then access this.data. Or any other property wrapped by defineReactive will access the propped property’s GET method. Let’s go back and see what the GET method looks like.

Note: I said something else was used defineReactive, which is related to vuex, which we’ll talk about later

get: function reactiveGetter () { const value = getter ? getter.call(obj) : Val // At this point, If (dep.target) {// computed watcher relies on Dep dep.depend() if (childOb) {childob.dep.depend ()} if for this.data (Array.isArray(value)) { dependArray(value) } } return value }Copy the code

I’m not going to explain it, because I’ve already written it in the code comments, but at this point we’ve gone through a dependency collection process, and we know how computed knows who it depends on. Finally, based on the notify call in the set method this. Data is proxied, you can change the value of this.data to update all computed property values that depend on this.

So, we can easily disassemble the process of obtaining dependencies and updating them according to the following code

Var vm = new Vue({el: '#example', data: {message: 'Hello'}, computed: {// Calculate attributes of the getter reversedMessage: Function () {// 'this' points to vm instance return this.message.split('').reverse().join()}}}) vm.reversedMessage // => olleH vm.message = 'World' // vm.reversedMessage // => dlroWCopy the code
  1. Initialize data and computed, proxy their SET and GET methods, respectively, and generate a unique DEP instance for all attributes in data.
  2. Generate a unique watcher for a reversedMessage in computed and save it to find vm._computedWatchers
  3. Access the reversedMessage, set dep. target to the Watcher of the reversedMessage, and call the reversedMessage method.
  4. The get method of the This. message proxy is called to add the deP of this.message to the watcher input reversedMessage, and the subs in the DEP add the Watcher
  5. Set vm.message = ‘World’ and invoke the message proxy’s set method to trigger notify of deP.
  6. Because it is a computed property, you just set dirty to true in watcher
  7. ReversedMessage gets the value watcher.dirty of the reversedMessage as true, and then calls watcher.evaluate() to get the new value.

This also explains why, in some cases, when computed is not accessed (or not dependent on templates), vue-tools finds no change in its computed value after modifying its this.data value, because its GET method is not triggered.

Vuex plug-in

With the introduction above, we can easily explain the principle of VUEX.

As we know, Vuex only exists as a plug-in for VUE. Unlike Redux,MobX and other libraries that can be applied to all frameworks, Vuex can only be used on VUE, largely because it is highly dependent on VUE’s computed dependency detection system and its plug-in system.

As we know from the official documentation, every Vue plug-in requires a public install method, and Vuex is no exception. $store for each component of the beforeCreate life cycle. For each component of the beforeCreate life cycle, this.$store for each component of the beforeCreate life cycle.

// src/store.js
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
Copy the code
Export default function (Vue) {const version = Number(vue.version.split ('.')[0]) if (version >= 2) { Vue.mixin({ beforeCreate: vuexInit }) } else { 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 = typeof options.store === 'function' ? options.store() : options.store } else if (options.parent && options.parent.$store) { this.$store = options.parent.$store } } }Copy the code

When we use vuex in business, we need to write something like this

const store = new Vuex.Store({
    state,
    mutations,
    actions,
    modules
});
Copy the code

So what exactly is vuex.store? Let’s look at his constructor first

// src/store.js constructor (options = {}) { const { plugins = [], strict = false } = options // store internal state this._committing = false this._actions = Object.create(null) this._actionSubscribers = [] this._mutations = Object.create(null) this._wrappedGetters = Object.create(null) this._modules = new ModuleCollection(options) this._modulesNamespaceMap = Object.create(null) this._subscribers = [] this._watcherVM = new Vue() const store = this const { dispatch, commit } = this this.dispatch = function boundDispatch (type, payload) { return dispatch.call(store, type, payload) } this.commit = function boundCommit (type, payload, options) { return commit.call(store, type, payload, options) } // strict mode this.strict = strict const state = this._modules.root.state // init root module. // this also recursively registers all sub-modules // and collects all module getters inside this._wrappedGetters installModule(this, State, [], this._modules.root) // // apply plugins plugins.foreach (plugin => plugin(this))}Copy the code

In addition to a bunch of initializations, we notice a line of code like resetStoreVM(this, state), which is the key to the entire VUex

// src/store.js function resetStoreVM (store, state, Hot) {// omit irrelevant code vue.config. silent = true store._vm = new Vue({data: {? state: state }, computed }) }Copy the code

After removing some extraneous code, we found that the state we passed in is essentially the data of a hidden VUE component. In other words, our commit operation essentially changes the data value of this component. In conjunction with computed above, after modifying the value of an object that is brokered by defineReactive, dirty is set to true in the dependent watcher that it collects, and the latest value is retrieved the next time the value in the watcher is accessed.

This explains why the object attributes of state in VUEX must be defined in advance. If an attribute is added midway through the state, it is not detected by the dependent system because the attribute is not defineReactive and cannot be updated.

Store._vm.$data.? State === store.state, we can do this in any project that contains vuex framework




conclusion

The overall idea of VUEX was born in Flux, but its implementation completely uses the responsive design of VUE itself. The dependent listening and collection belong to the proxy hijacking of the object Property set GET method by VUE. The store in Vuex is essentially a hidden vue component without a template.

If this article has helped you, please feel free to give a thumbs up. Thank you

Refer to the article

Have an in-depth understanding of Vue Computed attributes