Vuex 源码分析

2023-02-19 0 566

以后大体预测过了vue-cli 源标识符vue-router 源标识符, 这三个远距工具也是vue自然生态中较为关键的模块. 而前段时间即使销售业务上的须要, 碰触到了vuex的许多模块源标识符.vuex集中于MVC商业模式中的Model层, 明确规定大部份的统计数据操作方式要透过action-mutation-state change的销售业务流程来展开, 再紧密结合vue的统计数据快照单向存取优点来同时实现网页的展现预览:

Vuex 源码分析

那时就具体内容来预测呵呵其销售业务流程,写本文时, vuex 的版是 2.4.0

产品目录内部结构

关上 Vuex 工程项目, 先介绍下其产品目录内部结构,Vuex提供更多了十分强悍的状况管理工作机能, 源标识符标识符量却不多, 产品目录内部结构分割也很明晰. 先大体上如是说下各产品目录文档的机能:

module: 提供更多module第一类与module第一类树的建立机能plugins: 提供更多合作开发的远距应用领域程序helpers.js: 提供更多 mapGetters, mapActions 等 APIindex.js/index.esm.js: 源标识符主出口处文档mixin.js: 在 Vue 示例上转化成 storeutil.js: 提供更多 vuex 合作开发的一连串远距工具方式, 如 forEachValue/assert 等

出口处

在 2.4.0 中, vuex 提供更多了 UMD 和 ESM(ES module) 三个构筑出口处, 依次相关联 src/index.js 和 src/index.esm.js 文档. 在入口文档中, 主要就是求出 vuex 提供更多给 Vue 应用领域的 API:

// src/index.jsimport { Store, install } from ./storeimport { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from ./helpersexport default { Store, install, version: __VERSION__, mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers}

在 Vue 应用领域中, vuex 是作为 vue 的一个应用领域程序为 Vue 应用领域提供更多强悍的状况管理工作机能. 因而, 在能够使用 vuex 的机能以后, 先要在 Vue 应用领域的出口处文档中安装 vuex:

// …import Vue from vueimport Vuex from vuex// install vuex pluginVue.use(Vuex)// …

Vue 在安装应用领域程序时, 会去调用应用领域程序提供更多的 install 方式:

// src/store.jsimport applyMixin from ./mixin// …let Vue;export class Store { // … constructor (options = {}) { // … // 浏览器环境下的自动安装:to fix #731 if (!Vue && typeof window !== undefined && window.Vue) { install(window.Vue) } // … }}// …export function install (_Vue) { if (Vue) { if (process.env.NODE_ENV !== production) { console.error( [vuex] already installed. Vue.use(Vuex) should be called only once. ) } return } // Vue 变量赋值 Vue = _Vue applyMixin(Vue)}

在 src/store.js 文档中, 先声明了一个局部变量 Vue 来保存Vue 引用, 该变量有如下作用:

应用领域程序不必将 Vue.js 作为一个依赖打包作为避免重复安装的 vuex 的条件判断在 Store 中调用 vue 全局 API 的提供更多者建立 Vue 示例

在 install 方式中, 调用了 applyMixin 方式:

// src/mixins.jsexport default function (Vuook 的方式转化成 Vue.mixin({ beforeCreate: vuexInit }) } else { // 兼容 1.x // 使用自定义的 _init 方式并替换 Vue 第一类原型的_init方式,同时实现转化成 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 转化成 if (options.store) { this.$store = typeof options.store === function ? options.store() : options.store } else if (options.parent && options.parent.$store) { // 子模块从其父模块引用 $store 属性 this.$store = options.parent.$store } }}

applyMixin 方式的主要就机能将初始化 Vue 示例时传入的 store 设置到 this 第一类的 $store 属性上, 子模块则从其父模块引用$store 属性, 层层嵌套展开设置. 这样, 任何一个模块都能透过 this.$store 的方式访问 store 第一类了。

store 对象构造

store 第一类构造的源标识符定义在 src/store.js 中, 梳理源标识符以后, 先大体介绍下其构造销售业务流程。

环境判断

在 store 的构造函数中, vuex 先对构造 store 须要的许多环境变量展开断言:

import { forEachValue, isObject, isPromise, assert } from ./util//…let Vue; // …if (process.env.NODE_ENV !== production) { // 根据变量 Vue 的值判断是否已经安装过 vuex assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`) // 当前环境是否支持 Promise assert(typeof Promise !== undefined, `vuex requires a Promise polyfill in this browser.`) // 是否是透过 new 操作方式符来建立 store 第一类的 assert(this instanceof Store, `Store must be called with the new operator.`) }// …

assert 函数的定义是在 src/util 中:

export function assert (condition, msg) { if (!condition) throw new Error(`[vuex] ${msg}`)}
初始化变量

在环境变量判断之后, 在构造函数中会定义许多变量, 这些变量一部分来自 options, 一部分是内部定义:

import ModuleCollection from ./module/module-collection//…let Vt { state = {}} = optionsif (typeof state === function) { state = state()}/** * store 内部变量 */// 是否在进行提交状况标识this._committing = false // 用户定义的 actionsthis._actions = Object.create(null) // 用户定义的 mutationsthis._mutations = Object.create(null)// 用户定义的 gettersthis._wrappedGetters = Object.create(null)// 收集用户定义的 modulesthis._modules = new ModuleCollection(options)// 模块命名空间mapthis._modulesNamespaceMap = Object.create(null)// 存储大部份对 mutation 变化的订阅者this._subscribers = []// 建立一个 Vue 示例, 利用 $watch 监测 store 统计数据的变化this._watcherVM = new Vue()// …

收集 modules 时, 传入调用 Store 构造函数传入的 options 第一类, ModuleCollection 类的定义在 src/modules/module-collection.js 中:

import Module from ./moduleimport { assert, forEachValue } from ../utilexport default class ModuleCollection { constructor (rawRootModule) { // 注册根module this.register([], rawRootModule, false) } // … register (path, rawModule, runtime = true) { if (process.env.NODE_ENV !== production) { // 对 module 展开断言, 判断 module 是否符合要求 // module 的 getters/actions/ mutations 等字段是可遍历的第一类 // 且 key 的值类型是函数 assertRawModule(path, rawModule) } // 建立 module 第一类 const newModule = new Module(rawModule, runtime) if (path.length === 0) { this.root = newModule } else { const parent = this.get(path.slice(0, -1)) parent.addChild(path[path.length – 1], newModule) } // 递归建立子 module 第一类 if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule, runtime) }) } } // …}

ModuleCollection主要就将传入的options第一类整个构造为一个module第一类, 并循环调用register为其中的modules属性展开模块注册, 使其都成为module第一类, 最后options第一类被构造成一个完整的模块树. 详细源标识符可以查看module-collection.js.

Module 类的定义在 src/modules/module.js:

import { forEachValue } from ../utilexport default class Module { constructor (rawModule, runtime) { this.runtime = runtime this._children = Object.create(null) // 当前 module this._rawModule = rawModule // 当前 module 的 state const rawState = rawModule.state this.state = (typeof rawState === function ? rawState() : rawState) || {} } // … // 执行 installModule 时会用到的许多示例方式 forEachChild (fn) { forEachValue(this._children, fn) } forEachGetter (fn) { if (this._rawModule.getters) { forEachValue(this._rawModule.getters, fn) } } forEachAction (fn) { if (this._rawModule.actions) { forEachValue(this._rawModule.actions, fn) } } forEachMutation (fn) { if (this._rawModule.mutations) { forEachValue(this._rawModule.mutations, fn) } }}

详细源标识符可以查看module.js.

接着往下看:

// 存取 this 到 storeconst store = thisconst { dispatch, commit } = this// 确保 dispatch/commit 方式中的 this 第一类正确指向 storethis.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)}// …

上述标识符主要就是把 Store 类的 dispatch 和 commit 的方式的 this 指针指向当前 store 的示例上. 这样做的目的可以保证当我们在模块中透过 this.$store 直接调用 dispatch/commit 方式时, 能够使 dispatch/commit 方式中的 this 指向当前的 store 第一类而不是当前模块的 this.

dispatch 的机能是触发并传递许多参数(payload)给与 type 相关联的 action, 其具体内容同时实现如下:

// …dispatch (_type, _payload) {相关联的处理过的 action 函数集合 const entry = this._actions[type] if (!entry) { if (process.env.NODE_ENV !== production) { console.error(`[vuex] unknown action type: ${type}`) } return } // 执行 action 函数 return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload)} //…

unifyObjectStyle 的同时实现

而 commit 会将 action type 提交给相关联的 mutation, 然后执行相关联 mutation 函数修改 module 的状况, 其同时实现如下:

// …commit (_type, _payload, _options) { // 解析参数 const { type, payload, options } = unifyObjectStyle(_tst entry = this._mutations[type] if (!entry) { if (process.env.NODE_ENV !== production) { console.error(`[vuex] unknown mutation type: ${type}`) } return } // 执行 mutation 函数 this._withCommit(() => { entry.forEach(function commitIterator (handler) { handler(payload) }) }) // 执行大部份的订阅者函数 this._subscribers.forEach(sub => sub(mutation, this.state)) if ( process.env.NODE_ENV !== production && options && options.silent ) { console.warn( `[vuex] mutation type: ${type}. Silent option has been removed. ` + Use the filter functionality in the vue-devtools ) }} // …

_withCommit 的同时实现

梳理完 dispatch 和 commit, 接着看后面的标识符:

import devtoolPlugin from ./plugins/devtool// …// 是否开启严格商业模式(true/false)this.strict = strict// 安装 modulesinstallModule(this, state, [], this._modules.root)// 初始化 store._vm, 观测 state 和 getters 的变化resetStoreVM(this, state)// 安装应用领域程序plugins.forEach(plugin => plugin(this))if (Vue.config.devtools) { devtoolPlugin(this)}// …

后续的标识符主要就是安装 modules、vm 模块设置和安装透过 options 传入的应用领域程序以及根据 Vue 全局的 devtools 设置, 是否启用 devtoolPlugin 应用领域程序. 接下来就先预测下 vm 模块部分, 之后再预测安装 modules 的部分。

vm 模块设置

resetStoreVM 的定义如下:

function resetStoreVM (store, state, hot) { // 旧的 vm 示例 const oldVm = store._vm // 定义 getters 属性 store.getters = {getters, // 并新建 computed 第一类展开存储 getter 函数执行的结果, // 然后透过Object.defineProperty方式为 getters 第一类建立属性 // 使得我们透过 this.$store.getters.xxxgetter 能够访问到 store._vm[xxxgetters] forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism computed[key] = () => fn(store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) // 临时保存全局 Vue.config.silent 的配置 const silent = Vue.config.silent // 将全局的silent设置为 true, 取消这个 _vm 的大部份日志和警告 // in case the user has added some funky global mixins Vue.config.silent = true // 设置新的 vm, 传入 state // 把 computed 第一类作为 _vm 的 computed 属性, 这样就完成了 getters 的注册 store._vm = new Vue({ data: { $$state: state }, computed }) // 还原 silent 设置 Vue.config.silent = silent // enable strict mode for new vm if (store.strict) { // 严格商业模式下, 在mutation之外的地方修改 state 会报错 enableStrictMode(store) } // 销毁旧的 vm 示例 if (oldVm) { if (hot) { store._withCommit(() => { oldVm._data.$$state = null }) } Vue.nextTick(() => oldVm.$destroy()) }}

enableStrictMode 的同时实现

module 安装

安装 modules 是 vuex 初始化的核心. ModuleCollection 方式把通过 options 传入的 modules 属性对其展开 Module 处理后, installModule 方式则会将处理过的 modules 展开注册和安装, 其定义如下:

// …installModule(this, state, [], this._modules.root)//…function installModule (store, rootState, path, module, hot) { const isRoot = !path.length const namespace = store._modules.getNamespace(path) // register in namespace map if (module.namespaced) { 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(() => { Vue.set(parentState, moduleName, module.state) }) } // 设置上下文环境 const local = module.context = makeLocalContext(store, namespace, path) // 注册 mutations module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) // 注册 actions module.forEachAction((action, key) => { const namespacedType = namespace + key registerAction(store, namespacedType, action, local) }) // 注册 getters module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) // 递归安装子 module module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) })}// …

installModule 接收5个参数: store、rootState、path、module、hot. store 表示当前 Store 示例, rootState 表示根 state, path 表示当前嵌套模块的路径数组, module 表示当前安装的模块, hot 当动态改变 modules 或者热预览的时候为 true.

registerMutation

数集合:

mutations, 没有就建立一个空数组 const entry = store._mutations[type] || (store._mutations[type] = []) // push 处理过的 mutation handler entry.push(function wrappedMutationHandler (payload) { // 调用用户定义的 hanler, 并传入 state 和 payload 参数 handler.call(store, local.state, payload) })}
registerAction

该方式是对 store 的 action 的初始化:

tions, 没有就建立一个空数组 const entry = store._actions[type] || (store._actions[type] = []) // push 处理过的 action handler // 在模块中调用 action 则是调用 wrappedActionHandler entry.push(function wrappedActionHandler (payload, cb) { // 调用用户定义的 hanler, 并传入context第一类、payload 参数和回调函数 cb let res = handler.call(store, { dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload, cb) if (!isPromise(res)) { // 将 res 包装为一个 promise res = Promise.resolve(res) } // 当 devtools 开启的时候, 能捕获 promise 的报错 if (store._devtoolHook) { return res.catch(err => { store._devtoolHook.emit(vuex:error, err) throw err }) } else { // 返回处理结果 return res } })}

在调用用户定义的 action handler 时, 给改 handler 传入了三个参数: context 第一类, payload 和一个回调函数(很少会用到). context 第一类到 commit/dispatch 方式的原因.

registerGetter

该方式是对 store 的 getters 的初始化:

function registerGetter (store, type, rawGetter, local) { // 根据 type(module.getters 的 key) 判断 getter 是否存在 if (store._wrappedGetters[type]) { if (process.env.NODE_ENV !== production) { console.error(`[vuex] duplicate getter key: ${type}`) } return } // 包装 getter // 在模块中调用 getter 则是调用 wrappedGetter store._wrappedGetters[type] = function wrappedGetter (store) { // 调用用户定义的 getter 函数 return rawGetter( local.state, // local state local.getters, // local getters store.state, // root state store.getters // root getters ) }}
子 module 安装

注册完了根模块的 actions、mutations 以及 getters 后, 递归调用自身, 为子模块注册其state,actions、mutations 以及 getters 等.

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

远距函数

Vuex 除了提供更多我们 store 第一类外, 还对外提供更多了一连串以 mapXXX 命名的远距函数, 提供更多了操作方式 store 的各种属性的一连串语法糖. 远距函数的定义均在 src/helpers.js 中, 由于 mapXXX 等函数的同时实现大同小异, 责任编辑则只挑选常用的 mapActions 和 mapGetters 展开简单预测.

在预测以后, 先看三个函数的同时实现: normalizeNamespace 和 normalizeMap.

//normalizeNamespacefunction normalizeNamespace (fn) { return (namespace, map) => { if (typeof namespace !== string) { // 如果传给 mapXXX 的第一个参数不是一个字符串 // 则将 namespace 赋值给 map 参数并将 namespace 设置为空 map = namespace namespace = } else if (namespace.charAt(namespace.length – 1) !== /) { namespace += / } return fn(namespace, map) }}

normalizeNamespace 函数的主要就机能返回一个新的函数, 在新的函数中规范化 namespace 参数, 并调用函数参数fn.

// normalizeMapfunction normalizeMap (map) { return Array.isArray(map) ? map.map(key => ({ key, val: key })) : Object.keys(map).map(key => ({ key, val: map[key] }))}

normalizeMap 函数的作用则是将传递给 mapXXX 的参数统一转化为第一类返回, 例如:

// normalize actionsnormalizeMap([test, test1]) ==> {test: test, val: test}// normalize gettersnormalizeMap({ test: getTestValue, test2: getTestValue2,}) ==> {test: test, val: getTestValue}
mapGetters

该函数会将 store 中的 getter 映射到局部计算属性中:

export const mapGetters = normalizeNamespace((namespace, getters) => { // 返回结果 const res = {} // 遍历规范化参数后的第一类 // getters 就是传递给 mapGetters 的 map 第一类或者数组 normalizeMap(getters).forEach(({ key, val }) => { val = namespace + val res[key] = function mappedGetter () { // 一般不会传入 namespace 参数 if (namespace && !getModuleByNamespace(this.$store, mapGetters, namespace)) { return } // 如果 getter 不存在则报错 if (process.env.NODE_ENV !== production && !(val in this.$store.getters)) { console.error(`[vuex] unknown getter: ${val}`) return } // 返回 getter 值, store.getters 可见上文 resetStoreVM 的预测 return this.$store.getters[val] } // mark vuex getter for devtools res[key].vuex = true }) return res})
mapActions

该方式会将 store 中的 dispatch 方式映射到模块的 methods 中:

export const mapActions = normalizeNamespace((namespace, actions) => { // 返回结果 const res = {} // 遍历规范化参数后的第一类 // actions 就是传递给 mapActions 的 map 第一类或者数组 normalizeMap(actions).forEach(({ key, val }) => { res[key] = function mappedAction (…args) { // 保存 store dispatch 引用 let dispatch = this.$store.dispatch if (namespace) if (!module) { return } // 存取 module 上下文的 dispatch dispatch = module.context.dispatch } // 调用 action 函数 return typeof val === function ? val.apply(this, [dispatch].concat(args)) : dispatch.apply(this.$store, [val].concat(args)) } }) return res})

总结

[email protected] 的源标识符预测就暂时到这了,Vuex 的确是一个很强悍的状况管理工作远距工具,并且十分灵活,但有一个问题就是,如果严格按照单向统计数据流的方式展开合作开发,并参考官方文档给予的工程项目产品目录内部结构, 随着应用领域的复杂度越来越高, 合作开发者会写十分多的模板类标识符. 这个问题同样出那时Redux, 因而在 Redux 社区出现了诸如dvamirror这样的解决方案来减少模板类标识符的合作开发, 提高合作开发效率; 同时, React 社区也出现了更轻巧的状况管理工作远距工具,如statty

而在 Vuex 社区, 貌似还没有出现的类似的解决方案,因而个人在阅读过 Vuex 的源标识符之后,造了许多相关的轮子,欢迎参考和使用:

Revuejs: 基于 Vue.js 2 的一个轻量状况管理工作方案Lue: 基于 Vue.js 2 和 Vuex 2 的库

当然,也可以根据工程项目须要,采用其它的状况管理工作方案,例如mobx

举报/反馈

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务