vuex到底是什么?
使用vue也有一段时间了,但是对vue的理解似乎还是停留在初始状态,究其原因,不得不说是自己没有深入进去,理解本质,导致开发效率低,永远停留在表面, 更坏的结果就是refresh、restart。
首先说说什么是vue。 我对vue的理解是一个简单、易上手的开源框架。可以帮助我们快速构建单页面应用。由于MVVM的特点,在使用起来数据和视图的相互驱动使得页面的效果很好,避免了多余了http请求和繁琐的事件函数绑定,使我们在开发起来搞笑、畅快。
但是只是使用vue,还是存在问题的。比如在同一个页面间的组件之间状态的传递就会出现问题。 比如, 在一个购物页面,我们点击按钮增加物品的数量,然后在下方显示总价,并且为了组件的重用,按钮所在的组件和总价所在的组件是不同的, 那么如果希望两者进行传递数据,该怎么处理呢? 我们知道prop是用来从父组件向子组件进行传递的,所以不可行。在官网上也介绍了总线的使用,但是使用起来非常麻烦,所以vuex就排上了用场。 使用vuex,可以帮助我们在当前页面顺利的传递、分享状态,但是如果是在不同的页面,一旦刷新,页面的state数据就会丢失了,从这里,我们需要知道的时vuex只是状态管理的工具,而不是数据存储的工具,如果希望使用数据存储就必须要使用web Storage了。 官网所言如下:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
也就是说vuex仅仅是一个管理状态的工具,保证状态可以以一种可以预测的方式发生变化,并没有任何永久存储的功能。
下面,我们来解析源码
进入vue的github,首先,我们看到的就是这样的一个结构。
其中.github这种.开头的文件一般都是配置型的文件,比如.github、.babelrc、.eslintrc等等,这些文件往往是不需要在正式环境中使用的。 所以在看源码时是可以忽略的。
而build文件往往是用于创建一个项目时所需要的配置,比如vue-cli构建的项目中的各种构造(build)这个项目所需要配置的文件,并不是vuex的核心文件。
dist文件是最终生成的文件,比如我们使用vue-cli时,在npm run build之后生成的可以在实际项目中使用的文件就是dist文件,内容如下所示:
其中的vuex.js就是核心文件了,而 vuex.min.js 是压缩后的文件,在生产中使用。esm.js文件可能是在报错的时候见得最多了,他会主动跟踪我们的文件的错误,而vm也是非常常用的,vm可以理解为追踪,vue message等意思。 、
而logger之类的是日志记录。
接下来就是docs文件了,这个文件主要就是一些vuex的文档,内容如下:
其中assets中连接了vuex的官方网站。en是英文文档,fr是法国文档,ja是日本文档,kr是韩国文档,old中记录的时1.0的文档,ru是俄文文档,zh-cn就是中文文档,example就是官网的一些例子的使用。src就是vuex的源码文件夹,最后说。 test也不是重点,其中包含了e2e和jshint的代码检测工具。 types是typescript的写法介绍。 最后的一些文件都是无关紧要的了。
也就是说,读一个库的文件,最重要的是读取其src文件。如下所示:
我们先来对这个文件做一个整体介绍:
- module提供了module对象和module对象树的创建功能。
- plugins用于开发辅助插件,如state记录日志功能,显然这些都不是核心文件。
- helpers.js提供了mutation、action、getters的查找api。
- index.esm.js显然还是方便开发使用的追踪错误的文件。
- index.js即该核心文件的入口文件,看一个项目最开始就要看它的入口文件,所以一会我们最先从 index.js 开始分析。
- mixin.js提供了store在vue实例上的装载注入。
- util.js提供了find、assert等工具方法,在一个项目中util.js几乎是必不可少的。
index.js源码解读
之前说了,阅读一个项目就要从他的入口文件开始,因为这才是最为核心的地方。
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
index.js非常简短,一共就12行代码,但是通过这12行代码我们就可以知道vuex需要的是什么、提供的是什么了。而最最重要的时vuex提供了什么!
从export我们可以一目了然的看出vuex暴露了哪些api, 其中Store是最重要的vuex提供的状态存储类,我们使用vuex就是先要创建这个Store类,比如下面的代码:
Vue.use(Vuex)
export default new Vuex.Store({
state: {
totalPrice: 0,
items: [],
},
mutations: {
// ...
}
})
这里我们先引入vuex,然后Vue.use(Vuex),表示使用Vuex插件,而use可以引用就是因为我们暴露了install接口,这样在我们在执行Vue.use(Vuex)的时候就自动执行了install方法将Vuex作为引用。
然后导出了version即vuex的版本, 还有我们经常使用的mapState辅助函数、mapMutations辅助函数、mapGetters辅助函数、mapActions辅助函数、createNamespacedHelpers创建命名空间帮助函数。
而createNamespaceHelpers主要是为了创建不同模块的命令空间,解决命名冲突的问题。
正是因为vuex暴露了这些接口,所以我们在写代码时,才会出现如下所示的使用方法:
import {mapState, mapMutations, mapActions} from 'vuex'
export default {
methods: {
...mapMutations([
'UPDATE_CONTENT', "UPDATE_KINDS", "PUSH_TO_ITEMS", "UPDATE_CURINDEX"
]),
...mapActions([
'updateKinds', 'updateContent', 'updateAllContent', 'updateMall', 'getCurContent', 'updateAllContentSync'
]),
OK!到这就介绍了index.js,可以发现的时,这个入口文件的作用就是从不同的文件引入接口,然后再统一的从入口文件这暴露出去。就像我写的vue-toast一样,最后一定要暴露出module.exports = Toast; 这样,在Vue.use(vue-toast)之后才能开始使用这个暴露的接口。
从这个入口文件可以看出,他需要的时 store.js 和 helpers.js ,那么剩下的文件呢? 显示是被 store.js 和 helpers.js引入使用了。 所以沿着入口文件中import的文件,我们紧接着来看一看 store.js 文件。
store.js 源码解读
这个文件当属vux中最为重要的文件了。 阅读源码的好处在于更好的使用库并且理解其中的使用方法来提升自己,所以,对于这篇400余行的代码,我们采取精度的方式。下面开始 !
对于这种模块化的文件,我们最先要看的就是他需要的是什么,提供的是什么。 很容易可以看出,store.js 的意义就在于道出了一个Store对象 和 一个install对象。之前我们也提到过,是index.js中引入并导出的。
为了导出 Store对象和install对象, store.js引入了下面的一些模块:
import applyMixin from './mixin'
import devtoolPlugin from './plugins/devtool'
import ModuleCollection from './module/module-collection'
import { forEachValue, isObject, isPromise, assert } from './util'
至于这些模块具体是什么作用,我们先说一下后面在具体了解。 applyMixin的作用是在 vue 实例初始化时提供一个 $store 方法供vue调用。 deveollPlugin 的主要作用是利用vue的开发者工具来展示vuex中的数据状态,方便开发者的调试。 ModuleCollection的作用是支持 vuex 通过分模块的传入(collection就是收集的意思,将所有的vuex模块收集起来),可以是我们的开发更为高效,因为状态一多,分模块才更容易管理, 这才稍早的版本中是不存在的。而forEachValu、isObject等就是一些通用的方法,在下面解读的时候我们具体来讲。
let Vue // bind on install
定义局部 Vue 变量,用于判断是否已经装载和减少全局作用域查找。
环境判断:
接下来就是使用es6的语法,声明了一个store类:
export class Store {
constructor (options = {}) {
if (process.env.NODE_ENV !== 'production') {
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.`)
}
在开头讲了, assert是util.js中的,我们看看assert的源码:
export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
即给定一个条件,如果不满足,就抛出一个错误,这里使用了es6的模板字符串方法。
所以这里是在判断处于开发环境下,并且Vue是存在的,否则就会报错,提示没有Vue.use(Vuex)。 接着判断 Promise 是否可用,因为vuex是基于promise的,如果不可用,我们需要使用polyfill的方式实现promise。 这行代码的目的是为了确保 Promsie 可以使用的,因为 Vuex 的源码是依赖 Promise 的。Promise 是 es6 提供新的 API,由于现在的浏览器并不是都支持 es6 语法的,所以通常我们会用 babel 编译我们的代码,如果想使用 Promise 这个 特性,我们需要在 package.json 中添加对 babel-polyfill 的依赖并在代码的入口加上 import 'babel-polyfill'
这段代码。
接着就是 Store必须是用new操作符来创建的,所以这里的this时Store类型。
数据初始化:
构造函数接下来的代码是这样的:
const {
plugins = [],
strict = false
} = options
let {
state = {}
} = options
if (typeof state === 'function') {
state = state()
}
这里利用了对象的解构赋值,即options是我们通过Vue.use(vuex, {}) 中的后者可能传递的对象, 将options中的plugins赋值给当前的plugins, 将strict赋值给当前的strict,而 plugins = []和strict=false是默认值, 即如果options不存在时我们默认使用的值。
接下来定义了store的一些内部属性(我们习惯用_来表示内部属性):
this._committing = false
this._actions = Object.create(null)
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()
其中,_committing标志一个提交状态,作用是 state只能在 mutation 的回调函数中修改,而不能再其他地方修改, false就是不能修改。 this.actions是一个对象,它定义了用户的所有actions。 同样对于_mutations、_wrappedGetters。 subscribers定义了所有检测 mutations 变化的订阅者。 而 _watcherVM是vue对象的一个实例,使用$watch来检测变化的。
// 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)
}
这里的this绑定到了store实例上,然后定义了store的两个重要的方法 dispatch和commit。 接受的参数分别是 type ,即commit的类型以及dispatch的类型, 以及一个payload, 从源码就可以看出我们进行commit和dipatch的时候一定要传递一个payload。 这是很重要的。最后说明在 store 上调用。因为都是store的方法。
// strict mode
this.strict = strict
线下环境建议开启严格模式,线上环境建议关闭严格模式。
// 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)
// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
这几行代码的作用是递归地注册子模块。 而他的实现原理是什么呢? 下面看一看installModule的源码:
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)
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const namespacedType = namespace + key
registerAction(store, namespacedType, action, local)
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
即先判断是否是根目录, 如果path.length为0则为根目录,接着获得命名空间, 如果命名空间是存在的,我们就将模块注册到map中。
这里再判断如果不是根目录,并且不是hot,我们就通过getNestedState得到parentState以及moduleName,并且将module的state注册到parentState的moduleName中。由此来实现state注册。而getNestedState实现也非常简单:
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
即根据path的长度来决定返回的state。 然后就是环境的管理,
命名空间和根目录条件判断完毕后,接下来定义local变量和module.context的值,执行makeLocalContext方法,为该module设置局部的 dispatch、commit方法以及getters和state(由于namespace的存在需要做兼容处理)。
接下来我们就可以开始循环注册 mutations 和 actions了。
这样就可以使用了。 最后的介绍是关于一些基本方法的介绍。