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
- That is, Vue Use 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
- Get through inject
// 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
- 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
-
Mutation
-
Action
-
Getter
-
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.
- Push callback into subscription array
- 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
- Execute the function to commit
- 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
- Push the subscription object into this_ actionSubscribers
- 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.
-
Nuggets: Front end LeBron
-
Know: Front end LeBron
-
Continue to share technical blog, pay attention to WeChat official account. 👉🏻 Front end LeBron