This is the 7th day of my participation in the August Text Challenge.More challenges in August
Vuex introduction
Let’s start by introducing some of the concepts in VUex
Let’s follow vuEX’s workflow
- change
state
The only way is to submitmutations
- If asynchronous, dispatch.
actions
The essence is submissionmutations
- submit
mutations
After that, components can be dynamically renderedVue Components
Is there something missing? Yes, is there something missing
Vuex implementation
Now let’s implement a simple version of Vuex, modeled after vuex’s source code
You can check out the full source code here, along with the article
Building a Store
First we need to create a store with the following properties and methods
Define the createStore method, which simply creates a Store instance and passes options
export function createStore(options) {
return new Store(options)
}
Copy the code
Let’s look at the implementation of the Store class
The process is shown in figure
export class Store {
constructor(options = {}) {
const plugins = options.plugins || []
this._subscribers = []
this._actionSubscribers = []
this._actions = Object.create(null)
this._mutations = Object.create(null)
this.getters = Object.create(null)
/ / collection modules
this._modules = new ModuleCollection(options)
// Bind commit and dispatch to itself
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) {
return commit.call(store, type, payload, options)
}
const state = this._modules.root.state
/ / install the module
installModule(this, state, [], this._modules.root)
// Initialize state
resetStoreState(this, state)
// Application plug-in
plugins.forEach(plugin= > plugin(this))}}Copy the code
Collection of modules
Modules are collected using ModuleCollection to generate a module tree
// store.js
export class Store {
constructor(options = {}) {
this._modules = new ModuleCollection(options)
}
}
// module-collection.js
export default class ModuleCollection {
constructor(rawRootModule) {
this.register([], rawRootModule)
}
register(path, rawModule){}}Copy the code
A register function is called in the ModuleCollection to register each module
The Path argument refers to the current Moduel road dynamics, which can be used to determine hierarchies, and the rawModule is the original Module object
register(path, rawModule) {
const newModule = new Module(rawModule)
if (path.length === 0) {
// Root is defined as rawModule
this.root = newModule
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// If modules are passed in, recursively register the submodules
if (rawModule.modules) {
Object.keys(rawModule.modules).forEach(key= > {
const rawChildModule = rawModule.modules[key]
this.register(path.concat(key), rawChildModule)
})
}
}
Copy the code
In register, through rawModule we instantiate a Module object that has the following properties
_rawModule
: The original object passed in_children
Son: moduelsstate
: The state value of the original object passed ingetChild
: Ways to get sub-ModueladdChild
: Method to add child modules
export default class Module {
constructor(rawModule) {
this._rawModule = rawModule
this._children = Object.create(null)
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
}
Copy the code
When path.length === 0, use this newModule as the root value, which can also be called the parent Module
After root is set, it continues to determine if modules exist and, if so, recursively registers submodules
// If modules are passed in, recursively register the submodules
if (rawModule.modules) {
Object.keys(rawModule.modules).forEach(key= > {
const rawChildModule = rawModule.modules[key]
this.register(path.concat(key), rawChildModule)
})
}
Copy the code
In this case, set the key of each module to path to separate the hierarchy and add it to root
get(path) {
return path.reduce((module, key) = > {
return module.getChild(key)
}, this.root)
}
if (path.length === 0) {
// ...
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
Copy the code
We end up with a module tree like this
Bind commit and Dispatch
Go back to the store constructor code
// Bind commit and dispatch to itself
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) {
return commit.call(store, type, payload, options)
}
Copy the code
Encapsulate the dispatch and Commit methods in the replacement prototype, pointing this to the current Store object. The dispatch and commit methods are implemented as follows:
commit(type, payload) {
const mutation = { type, payload }
const entry = this._mutations[type]
if(! entry) {return
}
entry(payload)
this._subscribers.slice().forEach(sub= > sub(mutation, this.state))
}
Copy the code
We mentioned at the beginning that changing state requires a mutation commit, which implements this process
Dispath performs the same function as COMMIT, except that disPATH dispatches an action to submit the mutation state. We usually perform asynchronous functions in the action
dispatch(type, payload) {
const action = { type, payload }
const entry = this._actions[type]
if(! entry) {return
}
try {
this._actionSubscribers
.slice()
.filter(sub= > sub.before)
.forEach(sub= > sub.before(action, this.state))
} catch (error) {
console.error(e)
}
const result = entry(payload)
return new Promise((resolve, reject) = > {
result
.then(res= > {
try {
this._actionSubscribers
.filter(sub= > sub.after)
.forEach(sub= > sub.after(action, this.state))
} catch (error) {
console.error(e)
}
resolve(res)
})
.catch(error= > {
try {
this._actionSubscribers
.filter(sub= > sub.error)
.forEach(sub= > sub.error(action, this.state, error))
} catch (e) {
console.error(e)
}
reject(error)
})
})
}
Copy the code
In commit and DisPATH, each subscription function set _subscribers and _actionSubscribers is executed,
In the subscription function of _SUBSCRIBERS is passed the current mutation object and the current state, which are the parameters provided to the plug-in
_actionSubscribers also divides functions into before, After, and error types
The module is installed
The Module is installed to encapsulate the mutations, actions, and getters functions, passing in the required parameters
Encapsulation mutation
if (module._rawModule.mutations) {
Object.keys(module._rawModule.mutations).forEach(key= > {
const mutation = module._rawModule.mutations[key]
store._mutations[key] = payload= >
// Lazy get state
mutation.call(store, getNestedState(store.state, path), payload)
})
}
Copy the code
To encapsulate the action
if (module._rawModule.actions) {
Object.keys(module._rawModule.actions).forEach(key= > {
const action = module._rawModule.actions[key]
store._actions[key] = payload= > {
let res = action.call(
store,
{
dispatch: store.dispatch,
commit: store.commit,
getters: store.getters,
state: getNestedState(store.state, path),
},
payload
)
if(! (resinstanceof Promise)) {
res = Promise.resolve(res)
}
return res
}
})
}
Copy the code
Encapsulation getter
if (module._rawModule.getters) {
Object.keys(module._rawModule.getters).forEach(key= > {
const getter = module._rawModule.getters[key]
store.getters[key] = () = >
// Lazy get state
getter(getNestedState(store.state, path), store.getters)
})
}
Copy the code
Finally, recursively install the child Modules
Object.keys(module._children).forEach(key= >
installModule(store, rootState, path.concat(key), module._children[key])
)
Copy the code
It is important to note that state retrieval needs to be lazy, because state can change during vuEX use, and if you fix state when encapsulating functions, you will have unexpected behavior
Initialize the state
And then the resetStoreState function
export function resetStoreState(store, state) {
store._state = reactive({
data: state,
})
}
Copy the code
Make state reactive so that the view can be updated after Vuex changes state
The data responsiveness principle of VUe3 can be seen in my article analysis of the data responsiveness principle of VUE3
You might be a little confused by this, but why is there _state on the instance instead of state? There’s actually a getter in the store that gets the value of state
get state() {
return this._state.data
}
set state(v) {}
Copy the code
Here we can also see that setting state directly does not work
Application of plug-in
Let’s start by implementing a plug-in that prints changes before and after changing state: Logger
export const logger = store= > {
let prevState = deepClone(store.state)
store.subscribe((mutation, state) = > {
const nextState = deepClone(state)
const formattedTime = getFormattedTime()
const message = `${mutation.type}${formattedTime}`
console.log('%c prev state'.'color: #9E9E9E; font-weight: bold', prevState)
console.log('%c mutation'.'color: #03A9F4; font-weight: bold', message)
console.log('%c next state'.'color: #4CAF50; font-weight: bold', nextState)
prevState = nextState
})
}
Copy the code
Listen for mutation
Each plug-in is injected in the Store
plugins.forEach(plugin= > plugin(this)
Copy the code
To test the effect, if you clone the source code, you can execute it after installing the dependencies
yarn dev
Copy the code
Open the console to see the effect
Refer to the article
Vuex framework principle and source code analysis