导读
文章比较详细的介绍了vuex
、vue
源码调试方法和 Vuex
原理。并且详细介绍了 Vuex.use
安装和 new Vuex.Store
初始化、Vuex.Store
的全部API
(如dispatch
、commit
等)的实现和辅助函数 mapState
、mapGetters
、 mapActions
、mapMutations
createNamespacedHelpers
。
chrome 浏览器调试 vuex 源码方法
Vue文档:在 VS Code 中调试 Vue 项目
从上文中同理可得调试 vuex
方法,这里详细说下,便于帮助到可能不知道如何调试源码的读者。
可以把笔者的这个 vuex-analysis 源码分析仓库fork
一份或者直接克隆下来, git clone https://github.com/lxchuan12/vuex-analysis.git
其中文件夹
vuex
,是克隆官方的vuex
仓库dev
分支。
截至目前(2019年11月),版本是v3.1.2
,最后一次commit
是ba2ff3a3
,2019-11-11 11:51 Ben Hutton
。
包含笔者的注释,便于理解。
克隆完成后, 在vuex/examples/webpack.config.js
中添加devtool
配置。
// 新增devtool配置,便于调试
devtool: 'source-map',
output: {}
git clone https://github.com/lxchuan12/vuex-analysis.gitcd vuex
npm i
npm run dev
打开 http://localhost:8080/
点击你想打开的例子,例如:Shopping Cart => http://localhost:8080/shopping-cart/
打开控制面板 source 在左侧找到 webapck// . src 目录 store 文件 根据自己需求断点调试即可。
本文主要就是通过Shopping Cart
,(路径vuex/examples/shopping-cart
)例子调试代码的。
顺便提一下调试 vue 源码(v2.6.10)的方法
git clone https://github.com/vuejs/vue.git
克隆下来后将package.json
文件中的script
dev
命令后面添加这个 --sourcemap
。
{"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap"
}
git clone https://github.com/vuejs/vue.gitcd vue
npm i# 在 dist/vue.js 最后一行追加一行 //# sourceMappingURL=vue.js.map
npm run dev# 新终端窗口# 根目录下 全局安装http-server(一行命令启动服务的工具)
npm i -g http-server
hs -p 8100# 在examples 文件夹中把引用的vuejs的index.html 文件 vue.min.js 改为 vue.js# 或者把dist文件夹的 vue.min.js ,替换成npm run dev编译后的dist/vue.js# 浏览器打开 open http://localhost:8100/examples/# 打开控制面板 source 在左侧找到 src 目录 即vue.js源码文件 根据自己需求断点调试即可。
本小节大篇幅介绍调试方法。是因为真的很重要。会调试代码,看源码就比较简单了。关注主线调试代码,很容易看懂。强烈建议克隆笔者的这个仓库,自己调试代码,对着注释看,不调试代码,只看文章不容易吸收消化。
笔者也看了文章末尾笔者推荐阅读的文章,但还是需要自己看源代码,才知道这些文章哪里写到了,哪里没有细写。
正文开始~
vuex 原理
简单说明下 vuex
原理
<template><div>
count {{$store.state.count}}div>template>
每个组件(也就是Vue实例
)在beforeCreate
的生命周期中都混入(Vue.mixin)同一个Store实例
作为属性 $store
, 也就是为啥可以通过 this.$store.dispatch
等调用方法的原因。
最后显示在模板里的 $store.state.count
源码是这样的。
class Store{get state () {return this._vm._data.$$state
}
}
其实就是: vm.$store._vm._data.$$state.count
其中vm.$store._vm._data.$$state
是 响应式的。怎么实现响应式的?其实就是new Vue()
function resetStoreVM (store, state, hot) {// 省略若干代码store._vm = new Vue({
data: {
$$state: state
},
computed
})// 省略若干代码
}
这里的 state
就是 用户定义的 state
。这里的 computed
就是处理后的用户定义的 getters
。而 class Store
上的一些函数(API)主要都是围绕修改vm.$store._vm._data.$$state
和computed(getter)
服务的。
Vue.use 安装
笔者画了一张图表示下Vuex
对象,是Vue
的一个插件。
看到这里,恭喜你已经了解了
Vuex
原理。文章比较长,如果暂时不想关注源码细节,可以克隆一下本仓库代码git clone https://github.com/lxchuan12/vuex-analysis.git
,后续调试代码,点赞收藏到时想看了再看。
文档 Vue.use Vue.use(Vuex)
参数:{Object | Function} plugin 用法:
安装 Vue.js 插件。如果插件是一个对象,必须提供install
方法。如果插件是一个函数,它会被作为install
方法。install
方法调用时,会将 Vue 作为参数传入。
该方法需要在调用new Vue()
之前被调用。
当install
方法被同一个插件多次调用,插件将只会被安装一次。
根据断点调试,来看下Vue.use
的源码。
function initUse (Vue) {Vue.use = function (plugin) {var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));// 如果已经存在,则直接返回this也就是Vueif (installedPlugins.indexOf(plugin) > -1) {return this
}// additional parametersvar args = toArray(arguments, 1);// 把 this(也就是Vue)作为数组的第一项args.unshift(this);// 如果插件的install属性是函数,调用它if (typeof plugin.install === 'function') {plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') {// 如果插件是函数,则调用它// apply(null) 严格模式下 plugin 插件函数的 this 就是 nullplugin.apply(null, args);
}// 添加到已安装的插件installedPlugins.push(plugin);return this
};
}
install 函数
vuex/src/store.js
export function install (_Vue) {// Vue 已经存在并且相等,说明已经Vuex.use过if (Vue && _Vue === Vue) {// 省略代码:非生产环境报错,vuex已经安装return
}
Vue = _VueapplyMixin(Vue)
}
接下来看 applyMixin
函数
applyMixin 函数
vuex/src/mixin.js
export default function (Vue) {// Vue 版本号const version = Number(Vue.version.split('.')[0])if (version >= 2) {// 合并选项后 beforeCreate 是数组里函数的形式 [ƒ, ƒ]// 最后调用循环遍历这个数组,调用这些函数,这是一种函数与函数合并的解决方案。// 假设是我们自己来设计,会是什么方案呢。Vue.mixin({ beforeCreate: vuexInit })
} else {// 省略1.x的版本代码 ...
}/** * Vuex init hook, injected into each instances init hooks list. */function vuexInit () {const options = this.$options// store injection// store 注入到每一个Vue的实例中if (options.store) {this.$store = typeof options.store === 'function'? options.store(): options.store
} else if (options.parent && options.parent.$store) {this.$store = options.parent.$store
}
}
}
最终每个Vue
的实例对象,都有一个$store
属性。且是同一个Store
实例。
用购物车的例子来举例就是:
const vm = new Vue({
el: '#app',
store,render: h => h(App)
})console.log('vm.$store === vm.$children[0].$store', vm.$store === vm.$children[0].$store)// trueconsole.log('vm.$store === vm.$children[0].$children[0].$store', vm.$store === vm.$children[0].$children[0].$store)// trueconsole.log('vm.$store === vm.$children[0].$children[1].$store', vm.$store === vm.$children[0].$children[1].$store)// true
Vuex.Store 构造函数
先看最终 new Vuex.Store
之后的 Store
实例对象关系图:先大致有个印象。
export class Store {constructor (options = {}) {// 这个构造函数比较长,这里省略,后文分开细述
}
}
if (!Vue && typeof window !== 'undefined' && window.Vue) {install(window.Vue)
}
如果是 cdn script
方式引入vuex
插件,则自动安装vuex
插件,不需要用Vue.use(Vuex)
来安装。
// asset 函数实现export function assert (condition, msg) {if (!condition) throw new Error(`[vuex] ${msg}`)
}
if (process.env.NODE_ENV !== 'production') {// 可能有读者会问:为啥不用 console.assert,console.assert 函数报错不会阻止后续代码执行assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)assert(this instanceof Store, `store must be called with the new operator.`)
}
条件断言:不满足直接抛出错误
1.必须使用
Vue.use(Vuex)
创建store
实例。
2.当前环境不支持Promise
,报错:vuex
需要Promise polyfill
。
3.Store
函数必须使用new
操作符调用。
const {// 插件默认是空数组
plugins = [],// 严格模式默认是false
strict = false
} = options
从用户定义的new Vuex.Store(options)
取出plugins
和strict
参数。
// store internal state// store 实例对象 内部的 statethis._committing = false// 用来存放处理后的用户自定义的actoinsthis._actions = Object.create(null)// 用来存放 actions 订阅this._actionSubscribers = []// 用来存放处理后的用户自定义的mutationsthis._mutations = Object.create(null)// 用来存放处理后的用户自定义的 gettersthis._wrappedGetters = Object.create(null)// 模块收集器,构造模块树形结构this._modules = new ModuleCollection(options)// 用于存储模块命名空间的关系this._modulesNamespaceMap = Object.create(null)// 订阅this._subscribers = []// 用于使用 $watch 观测 gettersthis._watcherVM = new Vue()// 用来存放生成的本地 getters 的缓存this._makeLocalGettersCache = Object.create(null)
声明Store
实例对象一些内部变量。用于存放处理后用户自定义的actions
、mutations
、getters
等变量。
提一下
Object.create(null)
和{}
的区别。前者没有原型链,后者有。即Object.create(null).__proto__
是undefined
({}).__proto__
是Object.prototype
// bind commit and dispatch to selfconst store = thisconst { dispatch, commit } = thisthis.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)
}
给自己 绑定 commit
和 dispatch
为何要这样绑定 ?
说明调用commit
和dispach
的this
不一定是store
实例
这是确保这两个函数里的this
是store
实例
// 严格模式,默认是falsethis.strict = strict// 根模块的stateconst state = this._modules.root.state// init root module.// this also recursively registers all sub-modules// and collects all module getters inside this._wrappedGettersinstallModule(this, state, [], this._modules.root)// initialize the store vm, which is responsible for the reactivity// (also registers _wrappedGetters as computed properties)resetStoreVM(this, state)
上述这段代码 installModule(this, state, [], this._modules.root)
初始化 根模块。
并且也递归的注册所有子模块。
并且收集所有模块的getters
放在this._wrappedGetters
里面。
resetStoreVM(this, state)
初始化
store._vm
响应式的
并且注册_wrappedGetters
作为computed
的属性
plugins.forEach(plugin => plugin(this))
插件:把实例对象 store
传给插件函数,执行所有插件。
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtoolsif (useDevtools) {devtoolPlugin(this)
}
初始化 vue-devtool
开发工具。
参数 devtools
传递了取 devtools
否则取Vue.config.devtools
配置。
初读这个构造函数的全部源代码。会发现有三个地方需要重点看。分别是:
this._modules = new ModuleCollection(options)installModule(this, state, [], this._modules.root)resetStoreVM(this, state)
阅读时可以断点调试,赋值语句this._modules = new ModuleCollection(options)
,如果暂时不想看,可以直接看返回结果。installModule
,resetStoreVM
函数则可以断点调试。
class ModuleCollection
收集模块,构造模块树结构。
注册根模块 参数
rawRootModule
也就是Vuex.Store
的options
参数
未加工过的模块(用户自定义的),根模块
export default class ModuleCollection {constructor (rawRootModule) {// register root module (Vuex.Store options)this.register([], rawRootModule, false)
}
}
/** * 注册模块 * @param {Array} path 路径 * @param {Object} rawModule 原始未加工的模块 * @param {Boolean} runtime runtime 默认是 true */register (path, rawModule, runtime = true) {// 非生产环境 断言判断用户自定义的模块是否符合要求if (process.env.NODE_ENV !== 'production') {assertRawModule(path, rawModule)
}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)
}// register nested modules// 递归注册子模块if (rawModule.modules) {forEachValue(rawModule.modules, (rawChildModule, key) => {this.register(path.concat(key), rawChildModule, runtime)
})
}
}
class Module
// Base data struct for store's module, package with some attribute and method// store 的模块 基础数据结构,包括一些属性和方法export default class Module {constructor (rawModule, runtime) {// 接收参数 runtimethis.runtime = runtime// Store some children item// 存储子模块this._children = Object.create(null)// Store the origin module object which passed by programmer// 存储原始未加工的模块this._rawModule = rawModule// 模块 stateconst rawState = rawModule.state// Store the origin module's state// 原始Store 可能是函数,也可能是是对象,是假值,则赋值空对象。this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
}
经过一系列的注册后,最后 this._modules = new ModuleCollection(options)
this._modules
的值是这样的。笔者画了一张图表示:
installModule 函数
function installModule (store, rootState, path, module, hot) {// 是根模块const isRoot = !path.length// 命名空间 字符串const namespace = store._modules.getNamespace(path)if (module.namespaced) {// 省略代码:模块命名空间map对象中已经有了,开发环境报错提示重复// module 赋值给 _modulesNamespaceMap[namespace]store._modulesNamespaceMap[namespace] = module
}// ... 后续代码 移出来 待读解释
}
注册 state
// set state// 不是根模块且不是热重载if (!isRoot && !hot) {// 获取父级的stateconst parentState = getNestedState(rootState, path.slice(0, -1))// 模块名称// 比如 cartconst moduleName = path[path.length - 1]// state 注册store._withCommit(() => {// 省略代码:非生产环境 报错 模块 state 重复设置Vue.set(parentState, moduleName, module.state)
})
}
最后得到的是类似这样的结构且是响应式的数据 实例 Store.state 比如:
{// 省略若干属性和方法// 这里的 state 是只读属性 可搜索 get state 查看,上文写过
state: {
cart: {
checkoutStatus: null,
items: []
}
}
}
const local = module.context = makeLocalContext(store, namespace, path)
module.context
这个赋值主要是给helpers
中mapState
、mapGetters
、mapMutations
、mapActions
四个辅助函数使用的。
生成本地的dispatch、commit、getters和state。
主要作用就是抹平差异化,不需要用户再传模块参数。
遍历注册 mutation
module.forEachMutation((mutation, key) => {const namespacedType = namespace + keyregisterMutation(store, namespacedType, mutation, local)
})
/** * 注册 mutation * @param {Object} store 对象 * @param {String} type 类型 * @param {Function} handler 用户自定义的函数 * @param {Object} local local 对象 */function registerMutation (store, type, handler, local) {// 收集的所有的mutations找对应的mutation函数,没有就赋值空数组const entry = store._mutations[type] || (store._mutations[type] = [])// 最后 mutationentry.push(function wrappedMutationHandler (payload) {/** * mutations: { * pushProductToCart (state, { id }) { * console.log(state); * } * } * 也就是为什么用户定义的 mutation 第一个参数是state的原因,第二个参数是payload参数 */handler.call(store, local.state, payload)
})
}
遍历注册 action
module.forEachAction((action, key) => {const type = action.root ? key : namespace + keyconst handler = action.handler || actionregisterAction(store, type, handler, local)
})
/*** 注册 mutation* @param {Object} store 对象* @param {String} type 类型* @param {Function} handler 用户自定义的函数* @param {Object} local local 对象*/function registerAction (store, type, handler, local) {const entry = store._actions[type] || (store._actions[type] = [])// payload 是actions函数的第二个参数entry.push(function wrappedActionHandler (payload) {/** * 也就是为什么用户定义的actions中的函数第一个参数有 * { dispatch, commit, getters, state, rootGetters, rootState } 的原因 * actions: { * checkout ({ commit, state }, products) { * console.log(commit, state); * } * } */let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)/** * export function isPromise (val) { return val && typeof val.then === 'function' } * 判断如果不是Promise Promise 化,也就是为啥 actions 中处理异步函数 也就是为什么构造函数中断言不支持promise报错的原因 vuex需要Promise polyfill assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`) */if (!isPromise(res)) {
res = Promise.resolve(res)
}// devtool 工具触发 vuex:errorif (store._devtoolHook) {// catch 捕获错误return res.catch(err => {store._devtoolHook.emit('vuex:error', err)// 抛出错误throw err
})
} else {// 然后函数执行结果return res
}
})
}
遍历注册 getter
module.forEachGetter((getter, key) => {const namespacedType = namespace + keyregisterGetter(store, namespacedType, getter, local)
})
/** * 注册 getter * @param {Object} store Store实例 * @param {String} type 类型 * @param {Object} rawGetter 原始未加工的 getter 也就是用户定义的 getter 函数 * @examples 比如 cartProducts: (state, getters, rootState, rootGetters) => {} * @param {Object} local 本地 local 对象 */function registerGetter (store, type, rawGetter, local) {// 类型如果已经存在,报错:已经存在if (store._wrappedGetters[type]) {if (process.env.NODE_ENV !== 'production') {console.error(`[vuex] duplicate getter key: ${type}`)
}return
}// 否则:赋值store._wrappedGetters[type] = function wrappedGetter (store) {/** * 这也就是为啥 getters 中能获取到 (state, getters, rootState, rootGetters) 这些值的原因 * getters = { * cartProducts: (state, getters, rootState, rootGetters) => { * console.log(state, getters, rootState, rootGetters); * } * } */return rawGetter(local.state, // local statelocal.getters, // local gettersstore.state, // root statestore.getters // root getters
)
}
}
遍历注册 子模块
module.forEachChild((child, key) => {installModule(store, rootState, path.concat(key), child, hot)
})
resetStoreVM 函数
resetStoreVM(this, state, hot)
初始化
store._vm
响应式的
并且注册_wrappedGetters
作为computed
的属性
function resetStoreVM (store, state, hot) {// 存储一份老的Vue实例对象 _vmconst oldVm = store._vm// bind store public getters// 绑定 store.getterstore.getters = {}// reset local getters cache// 重置 本地getters的缓存store._makeLocalGettersCache = Object.create(null)// 注册时收集的处理后的用户自定义的 wrappedGettersconst wrappedGetters = store._wrappedGetters// 声明 计算属性 computed 对象const computed = {}// 遍历 wrappedGetters 赋值到 computed 上forEachValue(wrappedGetters, (fn, key) => {// use computed to leverage its lazy-caching mechanism// direct inline function use will lead to closure preserving oldVm.// using partial to return function with only arguments preserved in closure environment./** * partial 函数 * 执行函数 返回一个新函数 export function partial (fn, arg) { return function () { return fn(arg) } } */
computed[key] = partial(fn, store)// getter 赋值 keysObject.defineProperty(store.getters, key, {get: () => store._vm[key],// 可以枚举
enumerable: true // for local getters
})
})// use a Vue instance to store the state tree// suppress warnings just in case the user has added// some funky global mixins// 使用一个 Vue 实例对象存储 state 树// 阻止警告 用户添加的一些全局mixins// 声明变量 silent 存储用户设置的静默模式配置const silent = Vue.config.silent// 静默模式开启Vue.config.silent = truestore._vm = new Vue({
data: {
$$state: state
},
computed
})// 把存储的静默模式配置赋值回来Vue.config.silent = silent// enable strict mode for new vm// 开启严格模式 执行这句// 用 $watch 观测 state,只能使用 mutation 修改 也就是 _withCommit 函数if (store.strict) {enableStrictMode(store)
}// 如果存在老的 _vm 实例if (oldVm) {// 热加载为 trueif (hot) {// dispatch changes in all subscribed watchers// to force getter re-evaluation for hot reloading.// 设置 oldVm._data.$$state = nullstore._withCommit(() => {oldVm._data.$$state = null
})
}// 实例销毁Vue.nextTick(() => oldVm.$destroy())
}
}
到此,构造函数源代码看完了,接下来看 Vuex.Store
的 一些 API
实现。
Vuex.Store 实例方法
Vuex API 文档
commit
提交 mutation
。
commit (_type, _payload, _options) {// check object-style commit// 统一成对象风格const {type,payload,options
} = unifyObjectStyle(_type, _payload, _options)const mutation = { type, payload }// 取出处理后的用户定义 mutationconst entry = this._mutations[type]// 省略 非生产环境的警告代码 ...this._withCommit(() => {// 遍历执行entry.forEach(function commitIterator (handler) {handler(payload)
})
})// 订阅 mutation 执行this._subscribers.forEach(sub => sub(mutation, this.state))// 省略 非生产环境的警告代码 ...
}
commit
支持多种方式。比如:
store.commit('increment', {
count: 10
})// 对象提交方式store.commit({
type: 'increment',
count: 10
})
unifyObjectStyle
函数将参数统一,返回 { type, payload, options }
。
dispatch
分发 action
。
dispatch (_type, _payload) {// check object-style dispatch// 获取到type和payload参数const {type,payload
} = unifyObjectStyle(_type, _payload)// 声明 action 变量 等于 type和payload参数const action = { type, payload }// 入口,也就是 _actions 集合const entry = this._actions[type]// 省略 非生产环境的警告代码 ...try {this._actionSubscribers
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {if (process.env.NODE_ENV !== 'production') {console.warn(`[vuex] error in before action subscribers: `)console.error(e)
}
}const result = entry.length > 1? Promise.all(entry.map(handler => handler(payload))): entry[0](payload)return result.then(res => {try {this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {if (process.env.NODE_ENV !== 'production') {console.warn(`[vuex] error in after action subscribers: `)console.error(e)
}
}return res
})
}
replaceState
替换 store
的根状态,仅用状态合并或时光旅行调试。
replaceState (state) {this._withCommit(() => {this._vm._data.$$state = state
})
}
watch
响应式地侦听 fn 的返回值,当值改变时调用回调函数。
/** * 观测某个值 * @param {Function} getter 函数 * @param {Function} cb 回调 * @param {Object} options 参数对象 */watch (getter, cb, options) {if (process.env.NODE_ENV !== 'production') {assert(typeof getter === 'function', `store.watch only accepts a function.`)
}return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}
subscribe
订阅 store
的 mutation
。
subscribe (fn) {return genericSubscribe(fn, this._subscribers)
}
// 收集订阅者function genericSubscribe (fn, subs) {if (subs.indexOf(fn) < 0) {subs.push(fn)
}return () => {const i = subs.indexOf(fn)if (i > -1) {subs.splice(i, 1)
}
}
}
subscribeAction
订阅 store
的 action
。
subscribeAction (fn) {const subs = typeof fn === 'function' ? { before: fn } : fnreturn genericSubscribe(subs, this._actionSubscribers)
}
registerModule
注册一个动态模块。
/** * 动态注册模块 * @param {Array|String} path 路径 * @param {Object} rawModule 原始未加工的模块 * @param {Object} options 参数选项 */registerModule (path, rawModule, options = {}) {// 如果 path 是字符串,转成数组if (typeof path === 'string') path = [path]// 省略 非生产环境 报错代码// 手动调用 模块注册的方法this._modules.register(path, rawModule)// 安装模块installModule(this, this.state, path, this._modules.get(path), options.preserveState)// reset store to update getters...// 设置 resetStoreVMresetStoreVM(this, this.state)
}
unregisterModule
卸载一个动态模块。
/** * 注销模块 * @param {Array|String} path 路径 */unregisterModule (path) {// 如果 path 是字符串,转成数组if (typeof path === 'string') path = [path]// 省略 非生产环境 报错代码 ...// 手动调用模块注销this._modules.unregister(path)this._withCommit(() => {// 注销这个模块const parentState = getNestedState(this.state, path.slice(0, -1))Vue.delete(parentState, path[path.length - 1])
})// 重置 StoreresetStore(this)
}
hotUpdate
热替换新的 action
和 mutation
。
// 热加载hotUpdate (newOptions) {// 调用的是 ModuleCollection 的 update 方法,最终调用对应的是每个 Module 的 updatethis._modules.update(newOptions)// 重置 StoreresetStore(this, true)
}
组件绑定的辅助函数
文件路径:vuex/src/helpers.js
mapState
为组件创建计算属性以返回 Vuex store
中的状态。
export const mapState = normalizeNamespace((namespace, states) => {const res = {}// 非生产环境 判断参数 states 必须是数组或者是对象if (process.env.NODE_ENV !== 'production' && !isValidMap(states)) {console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
}normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {let state = this.$store.statelet getters = this.$store.getters// 传了参数 namespaceif (namespace) {// 用 namespace 从 store 中找一个模块。const module = getModuleByNamespace(this.$store, 'mapState', namespace)if (!module) {return
}
state = module.context.state
getters = module.context.getters
}return typeof val === 'function'? val.call(this, state, getters): state[val]
}// 标记为 vuex 方便在 devtools 显示// mark vuex getter for devtools
res[key].vuex = true
})return res
})
normalizeNamespace 标准化统一命名空间
function normalizeNamespace (fn) {return (namespace, map) => {// 命名空间没传,交换参数,namespace 为空字符串if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {// 如果是字符串,最后一个字符不是 / 添加 /// 因为 _modulesNamespaceMap 存储的是这样的结构。/** * _modulesNamespaceMap: cart/: {} products/: {} } * */
namespace += '/'
}return fn(namespace, map)
}
}
// 校验是否是map 是数组或者是对象。function isValidMap (map) {return Array.isArray(map) || isObject(map)
}
/** * Normalize the map * 标准化统一 map,最终返回的是数组 * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ] * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ] * @param {Array|Object} map * @return {Object} */function normalizeMap (map) {if (!isValidMap(map)) {return []
}return Array.isArray(map)? map.map(key => ({ key, val: key })): Object.keys(map).map(key => ({ key, val: map[key] }))
}
module.context
这个赋值主要是给 helpers
中 mapState
、mapGetters
、mapMutations
、mapActions
四个辅助函数使用的。
// 在构造函数中 installModule 中const local = module.context = makeLocalContext(store, namespace, path)
这里就是抹平差异,不用用户传递命名空间,获取到对应的 commit、dispatch、state、和 getters
getModuleByNamespace
function getModuleByNamespace (store, helper, namespace) {// _modulesNamespaceMap 这个变量在 class Store installModule 函数中赋值的const module = store._modulesNamespaceMap[namespace]if (process.env.NODE_ENV !== 'production' && !module) {console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}return module
}
看完这些,最后举个例子: vuex/examples/shopping-cart/components/ShoppingCart.vue
computed: {...mapState({checkoutStatus: state => state.cart.checkoutStatus
}),
}
没有命名空间的情况下,最终会转换成这样
computed: {
checkoutStatus: this.$store.state.checkoutStatus
}
假设有命名空间'ruochuan',
computed: {...mapState('ruochuan', {checkoutStatus: state => state.cart.checkoutStatus
}),
}
则会转换成:
computed: {
checkoutStatus: this.$store._modulesNamespaceMap.['ruochuan/'].context.checkoutStatus
}
mapGetters
为组件创建计算属性以返回 getter
的返回值。
export const mapGetters = normalizeNamespace((namespace, getters) => {const res = {}// 省略代码:非生产环境 判断参数 getters 必须是数组或者是对象normalizeMap(getters).forEach(({ key, val }) => {// The namespace has been mutated by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {return
}// 省略代码:匹配不到 getterreturn this.$store.getters[val]
}// mark vuex getter for devtools
res[key].vuex = true
})return res
})
举例:
computed: {...mapGetters('cart', {
products: 'cartProducts',
total: 'cartTotalPrice'
})
},
最终转换成:
computed: {
products: this.$store.getters['cart/cartProducts'],
total: this.$store.getters['cart/cartTotalPrice'],
}
mapActions
创建组件方法分发 action
。
export const mapActions = normalizeNamespace((namespace, actions) => {const res = {}// 省略代码:非生产环境 判断参数 actions 必须是数组或者是对象normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {// get dispatch function from storelet dispatch = this.$store.dispatchif (namespace) {const module = getModuleByNamespace(this.$store, 'mapActions', namespace)if (!module) {return
}
dispatch = module.context.dispatch
}return typeof val === 'function'? val.apply(this, [dispatch].concat(args)): dispatch.apply(this.$store, [val].concat(args))
}
})return res
})
mapMutations
创建组件方法提交 mutation
。mapMutations 和 mapActions 类似,只是 dispatch 换成了 commit。
let commit = this.$store.commit
commit = module.context.commitreturn typeof val === 'function'? val.apply(this, [commit].concat(args)): commit.apply(this.$store, [val].concat(args))
vuex/src/helpers
mapMutations
、mapActions
举例:
{
methods: {...mapMutations(['inc']),...mapMutations('ruochuan', ['dec']),...mapActions(['actionA'])...mapActions('ruochuan', ['actionB'])
}
}
最终转换成
{
methods: {inc(...args){return this.$store.dispatch.apply(this.$store, ['inc'].concat(args))
},dec(...args){return this.$store._modulesNamespaceMap.['ruochuan/'].context.dispatch.apply(this.$store, ['dec'].concat(args))
},actionA(...args){return this.$store.commit.apply(this.$store, ['actionA'].concat(args))
}actionB(...args){return this.$store._modulesNamespaceMap.['ruochuan/'].context.commit.apply(this.$store, ['actionB'].concat(args))
}
}
}
由此可见:这些辅助函数极大地方便了开发者。
createNamespacedHelpers
创建基于命名空间的组件绑定辅助函数。
export const createNamespacedHelpers = (namespace) => ({// bind(null) 严格模式下,napState等的函数 this 指向就是 null
mapState: mapState.bind(null, namespace),
mapGetters: mapGetters.bind(null, namespace),
mapMutations: mapMutations.bind(null, namespace),
mapActions: mapActions.bind(null, namespace)
})
就是把这些辅助函数放在一个对象中。
插件
插件部分文件路径是:vuex/src/plugins/devtool
vuex/src/plugins/logger
文章比较长了,这部分就不再叙述。具体可以看笔者的仓库 vuex-analysis vuex/src/plugins/
的源码注释。
总结
文章注释,在vuex-analysis源码仓库里基本都有注释分析,求个star
。再次强烈建议要克隆代码下来。
git clone https://github.com/lxchuan12/vuex-analysis.git
先把 Store
实例打印出来,看具体结构,再结合实例断点调试,事半功倍。
Vuex
源码相对不多,打包后一千多行,非常值得学习,也比较容易看完。
推荐阅读
vuex 官方文档
vuex github 仓库
美团明裔:Vuex框架原理与源码分析这篇文章强烈推荐,流程图画的很好
知乎黄轶:Vuex 2.0 源码分析这篇文章也强烈推荐,讲述的比较全面
小虫巨蟹:Vuex 源码解析(如何阅读源代码实践篇)这篇文章也强烈推荐,主要讲如何阅读源代码
染陌:Vuex 源码解析
网易考拉前端团队:Vuex 源码分析
yck:Vuex 源码深度解析
小生方勤:【前端词典】从源码解读 Vuex 注入 Vue 生命周期的过程
笔者精选文章
工作一年后,我有些感悟(写于2017年)
高考七年后、工作三年后的感悟
面试官问:JS的继承
前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并
学习 jQuery 源码整体架构,打造属于自己的 js 类库
学习underscore源码整体架构,打造属于自己的函数式编程类库
学习 lodash 源码整体架构,打造属于自己的函数式编程类库
学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
关于
作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论或者加作者vx:lxchuan12指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持,万分感谢。个人博客 https://lxchuan12.cn/posts 使用 vuepress
重构了,阅读体验可能更好些https://github.com/lxchuan12/blog,相关源码和资源都放在这里,求个 star
^_^~