[source code interpretation] read Vuex4 source code

Writing is not easy. Reprinting in any form is prohibited without the permission of the author!

Vuex4

Vuex is a commonly used state management library in Vue. After Vue3 is published, the state management library also sends Vuex4 adapted to Vue3

Principle of fast over Vuex3.x

  • Why can each component access store data through this.$store?
    • During beforeCreate, the store is injected through mixin
  • Why is the data in Vuex responsive
    • When creating a store, new Vue is called and a Vue instance is created, which is equivalent to borrowing Vue's response.
  • How does mapXxxx get the data and methods in the store
    • mapXxxx is just a syntax sugar, and the underlying implementation also obtains it from $store and returns it to calculated / methods.

Vuex4 use

Vue.useStore

  • Using Vuex in Vue3 Composition API
import { useStore } from 'vuex'

export default{
    setup(){
        const store = useStore();
    }
}

Research on Vuex4 principle

Remove redundant code and see the essence

How is Vuex4 injected into Vue

install

  • Vuex is used in Vue as a plug-in. When creating app, install is called
    • Add plugin to the plug-in list
    • Execute the plugin installation function
    • That is, the Vue.use function we often use
// Vue3 source app.use

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
  
    // Omit part of the code
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,

      version,
      
      // Omit part of the code

      use(plugin: Plugin, ...options: any[]) {
        if (installedPlugins.has(plugin)) {
          __DEV__ && warn(`Plugin has already been applied to target app.`)
        } else if (plugin && isFunction(plugin.install)) {
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          installedPlugins.add(plugin)
          plugin(app, ...options)
        } else if (__DEV__) {
          warn(
            `A plugin must either be a function or an object with an "install" ` +
              `function.`
          )
        }
        return app
      },
      // Omit part of the code
   }
}
  • The two implementations of install of the Store class are mounting to the global and accessing within components
    • Mount the store to global properties
    • See app.provide below for details
    • Get through inject
    • Get this.$store
// Vuex4 implementation plug-in install
install (app, injectKey) {
  // Get through inject
  app.provide(injectKey || storeKey, this)
  // Get this.$store
  app.config.globalProperties.$store = this

Schematic diagram of Provide / Inject architecture

Next, let's look at the implementation of provide

app.provide implementation

  • Each Vue component has a context object
  • Assign a value to the provides object in the context
  • createAppContext is a function to create App context
    • Mount the plug-in to the providers object of the app context in the form of key / value
    • When inputting, it is retrieved through the stored key
    • In the return body is an Option with some common options (mixins, components, etc.)
    • Vue's plug-in implements one of the most important services. The specific implementation methods are as follows:
// Vue3 app.provide implementation
provide(key, value) {
  // Warning if already exists
  if (__DEV__ && (key as string | symbol) in context.provides) {
    warn(
      `App already provides property with key "${String(key)}". ` +
        `It will be overwritten with the new value.`
    )
  }
  // Put the store into the provide of the context
  context.provides[key as string] = value
  return app
}

// Context context is a context object
const context = createAppContext()
export function createAppContext(): AppContext {
  return {
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      errorHandler: undefined,
      warnHandler: undefined,
      compilerOptions: {}
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null)
  }
}

Implementation of useStore

function useStore (key = null) {
  return inject(key !== null ? key : storeKey)
}

Vue.provide

  • Vue's provide API is also relatively simple, which is equivalent to assigning values directly through key/value
  • When the current instance provides the same as the parent instance, the connection is established through the prototype chain
// Vue3 provide implementation
function provide<T>(key: InjectionKey<T> | string | number, value: T) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    let provides = currentInstance.provides
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    // TS doesn't allow symbol as index type
    provides[key as string] = value
  }
}

Vue.inject

  • Take out the store through the key stored during provide
  • If there is a parent instance, the provider of the parent instance is taken; if there is no parent instance, the provider of the root instance is taken
// Vue3 inject implementation
function inject(
  key: InjectionKey<any> | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false
) {
  const instance = currentInstance || currentRenderingInstance
  if (instance) {
    // If there is a parent instance, the provider of the parent instance is taken; if there is no parent instance, the provider of the root instance is taken
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides

    // Take out the store through the key stored during provide
    if (provides && (key as string | symbol) in provides) {
      return provides[key as string]
    }
    // Omit part of the code
  } 
}

injection

  • Why does every component instance have a Store object?
    • Priority injection of parent providers
    • The bottom line is the providers injected into the app context
    • Providers are injected when creating component instances
function createComponentInstance(vnode, parent, suspense) {
    const type = vnode.type;
    const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
    const instance = {
        parent,
        appContext,
        // ...
        provides: parent ? parent.provides : Object.create(appContext.provides),
        // ...
    }
    // ...
    return instance;
}

API s such as provide, inject and getCurrentInstance can be introduced from vue for library development / high-level usage, which will not be repeated here.

Vuex4 execution mechanism

createStore

  • Starting with createStore
    • It can be found that the state in Vuex4 is the responsive data created through the reactive API, and Vuex3 is the new Vue instance
    • The implementation of dispatch and commit basically encapsulates a layer of execution, and the bottom layer is also executed through the store. Don't care too much
    • The reactive implementation of Vuex4 also borrows the reactive API of Vue3
// Vuex4 source code

export function createStore (options) {
    return new Store(options)
}
class Store{
    constructor (options = {}){
        // Omit some code
        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._makeLocalGettersCache = Object.create(null)
        
        // 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)
        }
        
        
        const state = this._modules.root.state
        installModule(this, state, [], this._modules.root);
        resetStoreState(this, state)
      
        // Omit some code
    }
}
function resetStoreState (store, state, hot) {
    // Omit some code
    store._state = reactive({
        data: state
    })
    // Omit some code
}

installModule

installModule mainly initializes each module in sequence, and the main function code has been highlighted

  1. Mutation
  2. Action
  3. Getter
  4. Child(install)
// Vuex4
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      if (__DEV__) {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          )
        }
      }
      parentState[moduleName] = module.state
    })
  }

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

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

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

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

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

Subscription Mechanism

After reading how Vuex4 is installed and injected, let's finally see how Vuex's subscription mechanism is implemented

  • Methods related to subscription mechanism mainly include
    • Subscription: subscribe and subscribeAction, which are used to subscribe to Mutation and Action respectively
    • Execution: commit and dispatch are used for execution respectively
  • Data items include:_ actionSubscribers,_ subscribers

subscribe

Subscribe to the mutation of the store. handler will be called after each mutation is completed, receiving mutation and the state after mutation as a parameter.

All subscription callback s will be put into this_ Subscribers, which can be put into the head / tail of the queue through the prepend option.

  1. Push callback into subscription array
  2. Returns a unsubscribe function
// Usage: this method returns a unsubscribe function
store.subscribe((action, state) => {
  console.log(action.type)
  console.log(action.payload)
}, { prepend: true }) 

// Subscription vuex4 source code implementation
subscribe (fn, options) {
  return genericSubscribe(fn, this._subscribers, options)
}

function genericSubscribe (fn, subs, options) {
  if (subs.indexOf(fn) < 0) {
    options && options.prepend
      ? subs.unshift(fn)
      : subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

Next, let's look at how to trigger the callback of these subscriptions when the commit is executed

  1. Execute the function to commit
  2. Execute this in turn_ Subscription callback in subscribers
// commit implementation
commit (_type, _payload, _options) {
  // check object-style commit
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  const entry = this._mutations[type]

  // Execute the function to commit
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })x 

    // Execute subscription function
  this._subscribers
    .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
    .forEach(sub => sub(mutation, this.state))
    
    // Omit several codes
}

subscribeAction

Subscribe to the store's action. The handler will call and receive the action description and the state of the current store when each action is distributed

Subscribable: pre execution, post execution and error

  1. Push the subscription object into this_ actionSubscribers
  2. Returns a unsubscribe function
// usage
store.subscribeAction({
  before: (action, state) => {
    console.log(`before action ${action.type}`)
  },
  after: (action, state) => {
    console.log(`after action ${action.type}`)
  },
  error: (action, state, error) => {
    console.log(`error action ${action.type}`)
    console.error(error)
  }
}, { prepend: true })

// Vuex4 source code implementation
subscribeAction (fn, options) {
  const subs = typeof fn === 'function' ? { before: fn } : fn
  return genericSubscribe(subs, this._actionSubscribers, options)
}

function genericSubscribe (fn, subs, options) {
  if (subs.indexOf(fn) < 0) {
    options && options.prepend
      ? subs.unshift(fn)
      : subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

How do I trigger these subscription functions when dispatch executes?

// Vuex4 source code implementation
dispatch (_type, _payload) {
  // check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)

  const action = { type, payload }
  const entry = this._actions[type]
  if (!entry) {
    if (__DEV__) {
      console.error(`[vuex] unknown action type: ${type}`)
    }
    return
  }

  // before subscription execution
  try {
    this._actionSubscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .filter(sub => sub.before)
      .forEach(sub => sub.before(action, this.state))
  } catch (e) {
    if (__DEV__) {
      console.warn(`[vuex] error in before action subscribers: `)
      console.error(e)
    }
  }

  // action execution
  const result = entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)

  return new Promise((resolve, reject) => {
    result.then(res => {
        // after subscription execution
      try {
        this._actionSubscribers
          .filter(sub => sub.after)
          .forEach(sub => sub.after(action, this.state))
      } catch (e) {
        if (__DEV__) {
          console.warn(`[vuex] error in after action subscribers: `)
          console.error(e)
        }
      }
      resolve(res)
    }, error => {
        // error subscription execution
      try {
        this._actionSubscribers
          .filter(sub => sub.error)
          .forEach(sub => sub.error(action, this.state, error))
      } catch (e) {
        if (__DEV__) {
          console.warn(`[vuex] error in error action subscribers: `)
          console.error(e)
        }
      }
      reject(error)
    })
  })
}

One sentence summary

Vuex3 - > vuex4. The main implementation method is to change mixin injection to provide / inject injection. Provide / Inject is not only used for Vuex implementation, but also for data transfer of deep components Tip: provide and inject bindings are not responsive. This is deliberate. However, if you pass in a listener object, the property of the object is still responsive.

Added by jjrosen on Thu, 09 Dec 2021 03:39:26 +0200