[source code interpretation] read Vuex4 source code

Writing is not easy. Reprinting in any form is prohibited without the permission of the author!
If you think the article is good, you are welcome to pay attention, praise and share!
Continue to share technical blog, pay attention to WeChat official account. 👉🏻 Front end LeBron
Original link

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

Fast pass vuex3 X principle

  • Why can every component pass this$ Store access to store data?
    • 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
    • That is, Vue Use function
      • Add plugin to the plug-in list
      • Execute the plugin installation function
// 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
    • Get through inject
      • See app below for details Provide explanation
    • Implement this$ Store get
      • Mount the store to global properties
// Vuex4 implementation plug-in install
install (app, injectKey) {
  // Get through inject
  app.provide(injectKey || storeKey, this)
  // Implement this$ Store get
  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
    • 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:
      • 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
// 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

  • Retrieve 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?
    • Providers are injected when creating component instances
      • Priority injection of parent providers
      • The bottom line is the providers injected into the app context
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.

Keywords: Javascript Vue Vue.js

Added by Fog Juice on Wed, 15 Dec 2021 01:01:57 +0200