你好,我是若川。这是
学习源码总体架构
第五篇。总体架构这词语好像有点大,姑且就算是源码总体结构吧,主要就是学习是代码总体结构,不深究其余不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。html
学习源码总体架构
系列文章以下:前端
1.学习 jQuery 源码总体架构,打造属于本身的 js 类库
2.学习 underscore 源码总体架构,打造属于本身的函数式编程类库
3.学习 lodash 源码总体架构,打造属于本身的函数式编程类库
4.学习 sentry 源码总体架构,打造属于本身的前端异常监控SDK
5.学习 vuex 源码总体架构,打造属于本身的状态管理库
6.学习 axios 源码总体架构,打造属于本身的请求库
7.学习 koa 源码的总体架构,浅析koa洋葱模型原理和co原理
8.学习 redux 源码总体架构,深刻理解 redux 及其中间件原理vue
感兴趣的读者能够点击阅读。下一篇多是学习 axios
源码。react
导读
文章比较详细的介绍了vuex
、vue
源码调试方法和 Vuex
原理。而且详细介绍了 Vuex.use
安装和 new Vuex.Store
初始化、Vuex.Store
的所有API
(如dispatch
、commit
等)的实现和辅助函数 mapState
、mapGetters
、 mapActions
、mapMutations
createNamespacedHelpers
。webpack
Vue文档:在 VS Code 中调试 Vue 项目
从上文中同理可得调试 vuex
方法,这里详细说下,便于帮助到可能不知道如何调试源码的读者。
能够把笔者的这个 vuex-analysis 源码分析仓库fork
一份或者直接克隆下来, git clone https://github.com/lxchuan12/vuex-analysis.git
ios
其中文件夹
vuex
,是克隆官方的vuex
仓库dev
分支。
截至目前(2019年11月),版本是v3.1.2
,最后一次commit
是ba2ff3a3
,2019-11-11 11:51 Ben Hutton
。
包含笔者的注释,便于理解。
git
克隆完成后, 在vuex/examples/webpack.config.js
中添加devtool
配置。github
// 新增devtool配置,便于调试 devtool: 'source-map', output: {} 复制代码
git clone https://github.com/lxchuan12/vuex-analysis.git cd vuex npm i npm run dev 复制代码
打开 http://localhost:8080/
点击你想打开的例子,例如:Shopping Cart => http://localhost:8080/shopping-cart/
打开控制面板 source 在左侧找到 webapck// . src 目录 store 文件 根据本身需求断点调试便可。
web
本文主要就是经过Shopping Cart
,(路径vuex/examples/shopping-cart
)例子调试代码的。面试
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.git cd 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
原理
<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)
服务的。
笔者画了一张图表示下Vuex
对象,是Vue
的一个插件。
看到这里,恭喜你已经了解了
Vuex
原理。文章比较长,若是暂时不想关注源码细节,能够克隆一下本仓库代码git clone https://github.com/lxchuan12/vuex-analysis.git
,后续调试代码,点赞收藏到时想看了再看。
文档 Vue.use Vue.use(Vuex)
参数: Object 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也就是Vue if (installedPlugins.indexOf(plugin) > -1) { return this } // additional parameters var 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 就是 null plugin.apply(null, args); } // 添加到已安装的插件 installedPlugins.push(plugin); return this }; } 复制代码
vuex/src/store.js
export function install (_Vue) { // Vue 已经存在而且相等,说明已经Vuex.use过 if (Vue && _Vue === Vue) { // 省略代码:非生产环境报错,vuex已经安装 return } Vue = _Vue applyMixin(Vue) } 复制代码
接下来看 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) // true console.log('vm.$store === vm.$children[0].$children[0].$store', vm.$store === vm.$children[0].$children[0].$store) // true console.log('vm.$store === vm.$children[0].$children[1].$store', vm.$store === vm.$children[0].$children[1].$store) // true 复制代码
先看最终 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 实例对象 内部的 state this._committing = false // 用来存放处理后的用户自定义的actoins this._actions = Object.create(null) // 用来存放 actions 订阅 this._actionSubscribers = [] // 用来存放处理后的用户自定义的mutations this._mutations = Object.create(null) // 用来存放处理后的用户自定义的 getters this._wrappedGetters = Object.create(null) // 模块收集器,构造模块树形结构 this._modules = new ModuleCollection(options) // 用于存储模块命名空间的关系 this._modulesNamespaceMap = Object.create(null) // 订阅 this._subscribers = [] // 用于使用 $watch 观测 getters this._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 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) } 复制代码
给本身 绑定 commit
和 dispatch
为什么要这样绑定 ?
说明调用commit
和dispach
的this
不必定是store
实例
这是确保这两个函数里的this
是store
实例
// 严格模式,默认是false this.strict = strict // 根模块的state const state = this._modules.root.state // init root module. // this also recursively registers all sub-modules // and collects all module getters inside this._wrappedGetters installModule(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.devtools if (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
函数则能够断点调试。
收集模块,构造模块树结构。
注册根模块 参数
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) }) } } 复制代码
// Base data struct for store's module, package with some attribute and method // store 的模块 基础数据结构,包括一些属性和方法 export default class Module { constructor (rawModule, runtime) { // 接收参数 runtime this.runtime = runtime // Store some children item // 存储子模块 this._children = Object.create(null) // Store the origin module object which passed by programmer // 存储原始未加工的模块 this._rawModule = rawModule // 模块 state const rawState = rawModule.state // Store the origin module's state // 原始Store 多是函数,也多是是对象,是假值,则赋值空对象。 this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} } } 复制代码
通过一系列的注册后,最后 this._modules = new ModuleCollection(options)
this._modules
的值是这样的。 笔者画了一张图表示:
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 } // ... 后续代码 移出来 待读解释 } 复制代码
// set state // 不是根模块且不是热重载 if (!isRoot && !hot) { // 获取父级的state const parentState = getNestedState(rootState, path.slice(0, -1)) // 模块名称 // 好比 cart const 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。
主要做用就是抹平差别化,不须要用户再传模块参数。
module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(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] = []) // 最后 mutation entry.push(function wrappedMutationHandler (payload) { /** * mutations: { * pushProductToCart (state, { id }) { * console.log(state); * } * } * 也就是为何用户定义的 mutation 第一个参数是state的缘由,第二个参数是payload参数 */ handler.call(store, local.state, payload) }) } 复制代码
module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(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:error if (store._devtoolHook) { // catch 捕获错误 return res.catch(err => { store._devtoolHook.emit('vuex:error', err) // 抛出错误 throw err }) } else { // 而后函数执行结果 return res } }) } 复制代码
module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(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 state local.getters, // local getters store.state, // root state store.getters // root getters ) } } 复制代码
module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) }) 复制代码
resetStoreVM(this, state, hot)
初始化
store._vm
响应式的
而且注册_wrappedGetters
做为computed
的属性
function resetStoreVM (store, state, hot) { // 存储一份老的Vue实例对象 _vm const oldVm = store._vm // bind store public getters // 绑定 store.getter store.getters = {} // reset local getters cache // 重置 本地getters的缓存 store._makeLocalGettersCache = Object.create(null) // 注册时收集的处理后的用户自定义的 wrappedGetters const 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 赋值 keys Object.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 = true store._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) { // 热加载为 true if (hot) { // dispatch changes in all subscribed watchers // to force getter re-evaluation for hot reloading. // 设置 oldVm._data.$state = null store._withCommit(() => { oldVm._data.$state = null }) } // 实例销毁 Vue.nextTick(() => oldVm.$destroy()) } } 复制代码
到此,构造函数源代码看完了,接下来看 Vuex.Store
的 一些 API
实现。
提交 mutation
。
commit (_type, _payload, _options) { // check object-style commit // 统一成对象风格 const { type, payload, options } = unifyObjectStyle(_type, _payload, _options) const mutation = { type, payload } // 取出处理后的用户定义 mutation const 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 }
。
分发 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 }) } 复制代码
替换 store
的根状态,仅用状态合并或时光旅行调试。
replaceState (state) { this._withCommit(() => { this._vm._data.$state = state }) } 复制代码
响应式地侦听 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) } 复制代码
订阅 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) } } } 复制代码
订阅 store
的 action
。
subscribeAction (fn) { const subs = typeof fn === 'function' ? { before: fn } : fn return genericSubscribe(subs, this._actionSubscribers) } 复制代码
注册一个动态模块。
/** * 动态注册模块 * @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... // 设置 resetStoreVM resetStoreVM(this, this.state) } 复制代码
卸载一个动态模块。
/** * 注销模块 * @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]) }) // 重置 Store resetStore(this) } 复制代码
热替换新的 action
和 mutation
。
// 热加载 hotUpdate (newOptions) { // 调用的是 ModuleCollection 的 update 方法,最终调用对应的是每一个 Module 的 update this._modules.update(newOptions) // 重置 Store resetStore(this, true) } 复制代码
文件路径:vuex/src/helpers.js
为组件建立计算属性以返回 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.state let getters = this.$store.getters // 传了参数 namespace if (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 } 复制代码
为组件建立计算属性以返回 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 } // 省略代码:匹配不到 getter return 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'], } 复制代码
建立组件方法分发 action
。
export const mapActions = normalizeNamespace((namespace, actions) => { const res = {} // 省略代码: 非生产环境 判断参数 actions 必须是数组或者是对象 normalizeMap(actions).forEach(({ key, val }) => { res[key] = function mappedAction (...args) { // get dispatch function from store let dispatch = this.$store.dispatch if (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 }) 复制代码
建立组件方法提交 mutation
。 mapMutations 和 mapActions 相似,只是 dispatch 换成了 commit。
let commit = this.$store.commit commit = module.context.commit return typeof val === 'function' ? val.apply(this, [commit].concat(args)) : commit.apply(this.$store, [val].concat(args)) 复制代码
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)) } } } 复制代码
因而可知:这些辅助函数极大地方便了开发者。
建立基于命名空间的组件绑定辅助函数。
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
、vue
源码调试方法和 Vuex
原理。而且详细介绍了 Vuex.use
安装和 new Vuex.Store
初始化、Vuex.Store
的所有API
(如dispatch
、commit
等)的实现和辅助函数 mapState
、mapGetters
、 mapActions
、mapMutations
createNamespacedHelpers
。
文章注释,在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 生命周期的过程
面试官问:JS的继承
面试官问:JS的this指向
面试官问:可否模拟实现JS的call和apply方法
面试官问:可否模拟实现JS的bind方法
面试官问:可否模拟实现JS的new操做符
做者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,惟善学。
我的博客-若川,使用vuepress
重构了,阅读体验可能更好些
掘金专栏,欢迎关注~
segmentfault
前端视野专栏,欢迎关注~
知乎前端视野专栏,欢迎关注~
github blog,相关源码和资源都放在这里,求个star
^_^~
可能比较有趣的微信公众号,长按扫码关注。也能够加微信 ruochuan12
,注明来源,拉您进【前端视野交流群】。
本文使用 mdnice 排版