学习 vuex 源码整体架构,打造属于自己的状态管理库

前言

这是学习源码整体架构第五篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。

其余四篇分别是:

  1. 学习 jQuery 源码整体架构,打造属于自己的 js 类库

  2. 学习underscore源码整体架构,打造属于自己的函数式编程类库

  3. 学习 lodash 源码整体架构,打造属于自己的函数式编程类库

  4. 学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK

感兴趣的读者可以点击阅读。下一篇可能是学习 axios 源码。

导读
文章比较详细的介绍了vuexvue源码调试方法和 Vuex 原理。并且详细介绍了 Vuex.use 安装和 new Vuex.Store 初始化、Vuex.Store 的全部API(如dispatchcommit等)的实现和辅助函数 mapStatemapGetters、 mapActionsmapMutations 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,最后一次commitba2ff3a32019-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.git
cd 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.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 原理

简单说明下 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.$$statecomputed(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)
// 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

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 = [],// 严格模式默认是falsestrict = false
} = options

从用户定义的new Vuex.Store(options) 取出pluginsstrict参数。

// 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实例对象一些内部变量。用于存放处理后用户自定义的actionsmutationsgetters等变量。

提一下 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),如果暂时不想看,可以直接看返回结果。installModuleresetStoreVM函数则可以断点调试。

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 中 mapStatemapGettersmapMutationsmapActions四个辅助函数使用的。
生成本地的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 polyfillassert(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.stategetters = module.context.getters}return typeof val === 'function'? val.call(this, state, getters): state[val]}// 标记为 vuex 方便在 devtools 显示// mark vuex getter for devtoolsres[key].vuex = true})return res
})

normalizeNamespace 标准化统一命名空间

function normalizeNamespace (fn) {return (namespace, map) => {// 命名空间没传,交换参数,namespace 为空字符串if (typeof namespace !== 'string') {map = namespacenamespace = ''} 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 中 mapStatemapGettersmapMutationsmapActions四个辅助函数使用的。

// 在构造函数中 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 normalizeNamespaceval = namespace + valres[key] = function mappedGetter () {if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {return}// 省略代码:匹配不到 getterreturn this.$store.getters[val]}// mark vuex getter for devtoolsres[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.commit
return typeof val === 'function'? val.apply(this, [commit].concat(args)): commit.apply(this.$store, [val].concat(args))

vuex/src/helpers

mapMutationsmapActions 举例:

{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 指向就是 nullmapState: 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/ 的源码注释。

总结

文章比较详细的介绍了vuexvue源码调试方法和 Vuex 原理。并且详细介绍了 Vuex.use 安装和 new Vuex.Store 初始化、Vuex.Store 的全部API(如dispatchcommit等)的实现和辅助函数 mapStatemapGetters、 mapActionsmapMutations 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 生命周期的过程

笔者精选文章

工作一年后,我有些感悟(写于2017年)

高考七年后、工作三年后的感悟

面试官问:JS的继承

前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

学习 jQuery 源码整体架构,打造属于自己的 js 类库

学习underscore源码整体架构,打造属于自己的函数式编程类库

学习 lodash 源码整体架构,打造属于自己的函数式编程类库

学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客 https://lxchuan12.cn/posts 使用 vuepress重构了,阅读体验可能更好些
https://github.com/lxchuan12/blog,相关源码和资源都放在这里,求个 star^_^~

欢迎加微信交流 微信公众号

可能比较有趣的微信公众号,长按扫码关注。也可以加微信 lxchuan12,注明来源,拉您进【前端视野交流群】。

左边是个人微信号  lxchuan12,右边是公众号【若川视野】

由于公众号限制外链,点击阅读原文,或许阅读体验更佳,觉得文章不错,可以点个在看呀^_^

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/274412.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VMware workstation 8.0上安装VMware ESXI5.0

首先&#xff0c;在VMware的官网上注册&#xff0c;下载VMware ESXI的安装包vmware&#xff0d;vmvisor&#xff0d;installer&#xff0d;5.0.0&#xff0d;469512.x86_64.iso&#xff0c;它是iso文件&#xff0c;刻盘进行安装&#xff0c;安装过程中&#xff0c;会将硬盘全部…

最新ui设计趋势_10个最新且有希望的UI设计趋势

最新ui设计趋势重点 (Top highlight)Recently, I’ve spent some time observing the directions in which UI design is heading. I’ve stumbled across a few very creative, promising and inspiring trends that, in my opinion, will shape the UI design in the nearest…

学习 axios 源码整体架构,打造属于自己的请求库

前言这是学习源码整体架构系列第六篇。整体架构这词语好像有点大&#xff0c;姑且就算是源码整体结构吧&#xff0c;主要就是学习是代码整体结构&#xff0c;不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。学习源码整体架构系列文章如下&#xff1a;1.…

404 错误页面_如何设计404错误页面,以使用户留在您的网站上

404 错误页面重点 (Top highlight)网站设计 (Website Design) There is a thin line between engaging and enraging when it comes to a site’s 404 error page. They are the most neglected of any website page. The main reason being, visitors are not supposed to end…

学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

前言这是学习源码整体架构系列第七篇。整体架构这词语好像有点大&#xff0c;姑且就算是源码整体结构吧&#xff0c;主要就是学习是代码整体结构&#xff0c;不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。学习源码整体架构系列文章如下&#xff1a;1.…

公网对讲机修改对讲机程序_更少的对讲机,对讲机-更多专心,专心

公网对讲机修改对讲机程序重点 (Top highlight)I often like to put a stick into the bike wheel of the UX industry as it’s strolling along feeling proud of itself. I believe — strongly — that as designers we should primarily be doers not talkers.我经常喜欢在…

若川知乎问答:2年前端经验,做的项目没什么技术含量,怎么办?

知乎问答&#xff1a;做了两年前端开发&#xff0c;平时就是拿 Vue 写写页面和组件&#xff0c;简历的项目经历应该怎么写得好看&#xff1f;以下是我的回答&#xff0c;阅读量5000&#xff0c;所以发布到公众号申明原创。题主说的2年经验做的东西没什么技术含量&#xff0c;应…

ui设计基础_我不知道的UI设计的9个重要基础

ui设计基础重点 (Top highlight)After listening to Craig Federighi’s talk on how to be a better software engineer I was sold on the idea that it is super important for a software engineer to learn the basic principles of software design.听了克雷格费德里希(C…

C# 多线程控制 通讯 和切换

一.多线程的概念   Windows是一个多任务的系统&#xff0c;如果你使用的是windows 2000及其以上版本&#xff0c;你可以通过任务管理器查看当前系统运行的程序和进程。什么是进程呢&#xff1f;当一个程序开始运行时&#xff0c;它就是一个进程&#xff0c;进程所指包括运行中…

vue路由匹配实现包容性_包容性设计:面向老年用户的数字平等

vue路由匹配实现包容性In Covid world, a lot of older users are getting online for the first time or using technology more than they previously had. For some, help may be needed.在Covid世界中&#xff0c;许多年长用户首次上网或使用的技术比以前更多。 对于某些人…

IPhone开发 用子类搞定不同的设备(iphone和ipad)

用子类搞定不同的设备 因为要判断我们的程序正运行在哪个设备上&#xff0c;所以&#xff0c;我们的代码有些混乱了&#xff0c;IF来ELSE去的&#xff0c;记住&#xff0c;将来你花在维护代码上的时间要比花在写代码上的时间多&#xff0c;如果你的项目比较大&#xff0c;且IF语…

见证开户_见证中的发现

见证开户Each time we pick up a new video game, we’re faced with the same dilemma: “How do I play this game?” Most games now feature tutorials, which can range from the innocuous — gently introducing each mechanic at a time through natural gameplay — …

facebook有哪些信息_关于Facebook表情表情符号的所有信息

facebook有哪些信息Ever since worldwide lockdown and restriction on travel have been imposed, platforms like #Facebook, #Instagram, #Zoom, #GoogleDuo, & #Whatsapp have become more important than ever to connect with your loved ones (apart from the sourc…

M2总结报告

团队成员 李嘉良 http://home.cnblogs.com/u/daisuke/ 王熹 http://home.cnblogs.com/u/vvnx/ 王冬 http://home.cnblogs.com/u/darewin/ 王泓洋 http://home.cnblogs.com/u/fiverice/ 刘明 http://home.cnblogs.com/u/liumingbuaa/ 由之望 http://www.cnbl…

react动画库_React 2020动画库

react动画库Animations are important in instances like page transitions, scroll events, entering and exiting components, and events that the user should be alerted to.动画在诸如页面过渡&#xff0c;滚动事件&#xff0c;进入和退出组件以及应提醒用户的事件之类的…

线框模型_进行计划之前:线框和模型

线框模型Before we start developing something, we need a plan about what we’re doing and what is the expected result from the project. Same as developing a website, we need to create a mockup before we start developing (coding) because it will cost so much…

工作经验教训_在设计工作五年后获得的经验教训

工作经验教训This June it has been five years since I graduated from college. Since then I’ve been working as a UX designer for a lot of different companies, including a start-up, an application developer, and two consultancy firms.我从大学毕业已经五年了&a…

中文排版规则_非设计师的5条排版规则

中文排版规则01仅以一种字体开始 (01 Start with only one font) The first tip for non-designers dealing with typography is simple and will make your life much easier: Stop combining different fonts you like individually and try using only one font in your fut…

基本响应性的Web设计测试工具

在重新设计页面的过程中。要使页面完全响应的设计&#xff08;这意味着它会重新调整大小根据浏览器的尺寸和方向&#xff09;。如iPhone和iPad的移动电话和平板电脑我碰到了一些非常方便的响应设计工具&#xff0c;帮我测试网站在不同的屏幕响应。下面的这些响应的网页设计工具…

ux设计_声音建议:设计UX声音的快速指南

ux设计Mating calls, warning grunts, and supportive coos are some of the sounds heard throughout the animal kingdom. All species use finely-tuned noises to communicate to one another and inform others of an action or behavior. We humans aren’t all that dif…