不少时候咱们在开发一个Vue项目的时候,用一个Vue实例封装的EventBus来处理事件的传递从而达到组件间状态的共享。可是随着业务的复杂度提高,组件间共享的状态变得难以追溯和维护。所以咱们须要将这些共享的状态经过一个全局的单例对象保存下来,在经过指定的方法去更新状态更新组件。javascript
既然都说vuex是解决组件间数据通讯的一种方式,那咱们先来回顾下组件间通讯的几种方法:前端
这种方法咱们能够直接将父组件的值传递给子组件,并在子组件中调用。很明显,props是一种单向的数据绑定,而且子组件不能去修改props的值。在vue1.x中能够经过.async来实现双向绑定,可是这种双向的绑定很难去定位数据错误的来源,在vue2.3.0版本又加回了.async。vue
// 父组件 <Child name="hahaha" /> // 子组件 <div>{{name}}</div> // ... props: ['name'] // ...
$on $emit
若是子组件向父组件传递数据,咱们能够经过$emit
和$on
,在子组件注册事件,在父组件监听事件并做回调。java
// 父组件 <Child @getName="getNameCb" /> // ... getNameCb(name) { console.log('name'); } // 子组件 someFunc() { this.$emit('getName', 'hahahah'); }
前面两种方式很容易就解决了父子组件的通讯问题,可是很难受的是,处理兄弟组件或者是祖孙组件的通讯时你须要一层一层的去传递props,一层一层的去$emit
。那么其实就可使用EventBus了,EventBus其实是一个Vue的实例,咱们经过Vue实例的$emit
和$on
来进行事件的发布订阅。可是问题也很明显,过多的使用EventBus也会形成数据源难以追溯的问题,而且不及时经过$off
注销事件的化,也会发生不少奇妙的事情。vue-router
import EventBus from '...'; // 某一个组件 // ... mounted() { EventBus.$on('someevent', (data) => { // ... }) } // ... // 某另外一个组件 // ... someFunc() { EventBus.$emit('someevent', 'hahahah'); } // ...
接下来就是咱们要讲的Vuex了,以上这些问题Vuex均可以解决,Vuex也是Vue官方团队维护的Vue全家桶中的一员,做为Vue的亲儿子,Vuex毫无疑问是很是适合Vue项目的了。可是Vuex也不是完美的,毫无疑问在应用中加一层Store或多或少的都会增长学习和维护的成本,而且说白了一个小项目没几个组件,Vuex只会增长你的代码量,酌情使用吧。下面就进入到咱们Vuex源码学习的正文了。vuex
回顾一下Vuex的设计原理。咱们把组件间共享的状态存储到Vuex的state中,而且组件会根据这个state的值去渲染。当须要更新state的时候,咱们在组件中调用Vuex提供的dispatch方法去触发action,而在action中去经过commit方法去提交一个mutation,最后经过mutation去直接修改state,组件监听到state的更新最后更新组件。须要注意的有,mutaion不能执行异步操做,异步操做须要放到action中去完成;直接修改state的有且仅有mutation。(具体的使用方法笔者就不去啰嗦了,官方文档写的很详细,还有中文版,为啥不看...)数据库
在笔者看来,Vuex的做用是用来解决组件间状态的共享,使项目更加利于维护,一样也是贯彻单向数据流这个理念。但其实从功能上讲,Vuex也像是一个前端的“数据库”,咱们在使用Vuex时很像是后端同窗对库的增删改查。redux
在Vue的项目中,咱们也能够去使用Redux等来处理共享的状态,甚至是能够本身简单封装一个工具来处理状态,毕竟引入Vuex对开发同窗来讲也是有必定成本的。可是归根到底都是单向数据流的思想,一通则百通。后端
插个题外话,笔者在研究Vue ssr的时候不想去用Vuex作先后端状态共享,因而基于EventBus的思想对Vue实例进行了封装也一样实现了Vuex的功能,有兴趣的同窗能够看下。戳这里。数组
首先咱们将挂载完Vuex实例的Vue实例打印出来看看挂载完增长了哪些东西。
这里不一样于vue-router会在Vue的实例上增长不少的自定义属性,有的仅仅是一个$store属性,指向初始化的Vuex实例。
拿到一个项目的源码咱们要先去浏览他它的目录结构:
其中src是咱们的源码部分:
一般在构建包含Vuex的程序的时候会这么写:
import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); const store = new Vuex({ state: {...}, mutations: {...}, actions: {...}, }); new Vue({ store, template, }).$mount('#app')
用过redux的小伙伴能够发现Vuex采用的是面向对象化的配置方式,不一样于redux那种“偏函数式的初始化”,能更容易的让开发者理解。而且Vuex是以插件的形式安装在Vue实例上。
在store.js中定义了一个符合Vue插件机制的导出函数install,而且封装了一个beforeCreate的mixin。
源码位置:/src/store.js /src/mixin.js
// store.js // ... // 绑定一个Vue实例; // 不用将Vue打包进项目即可以使用Vue的提供的一些静态方法; let Vue // ... // Vue 插件机制 export function install (_Vue) { if (Vue && _Vue === Vue) { if (process.env.NODE_ENV !== 'production') { console.error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ) } return } Vue = _Vue // 封装mixin挂载$store applyMixin(Vue) } // mixin.js export default function (Vue) { // 获取版本号 const version = Number(Vue.version.split('.')[0]) if (version >= 2) { Vue.mixin({ beforeCreate: vuexInit }) } else { // 兼容低版本的Vue const _init = Vue.prototype._init Vue.prototype._init = function (options = {}) { options.init = options.init ? [vuexInit].concat(options.init) : vuexInit _init.call(this, options) } } // 封装mixin; // 绑定$store实例; // 子组件的$store也始终指向根组件挂载的store实例; function vuexInit () { const options = this.$options // store injection if (options.store) { // store多是一个工厂函数,vue ssr中避免状态交叉污染一般会用工厂函数封装store; this.$store = typeof options.store === 'function' ? options.store() : options.store } else if (options.parent && options.parent.$store) { // 子组件从其父组件引用$store属性,嵌套设置 this.$store = options.parent.$store } } }
这里其实作的很简单就是在beforeCreate钩子中为Vue实例绑定了一个$store属性指向咱们定义的Store实例上。此外也能够看到Vuex也采用了很常见的导出一个Vue实例,从而不将Vue打包进项目就能使用Vue提供的一些方法。
实例化Store类,咱们先来看Store类的构造函数:
源码位置:/src/store.js
constructor (options = {}) { // 若是window上有Vue实例,直接安装插件; if (!Vue && typeof window !== 'undefined' && window.Vue) { install(window.Vue) } 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.`) } // 实例化store时传入的配置项; const { plugins = [], strict = false } = options // store internal state // 收集commit this._committing = false // 收集action this._actions = Object.create(null) // action订阅者 this._actionSubscribers = [] // 收集mutation this._mutations = Object.create(null) // 收集getter this._wrappedGetters = Object.create(null) // 收集module this._modules = new ModuleCollection(options) this._modulesNamespaceMap = Object.create(null) this._subscribers = [] // 用以处理状态变化的Vue实例 this._watcherVM = new Vue() // 将dispatch和commit调用的this指向Store实例; 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) } // strict mode this.strict = strict // 获取state const state = this._modules.root.state // 主要做用就是生成namespace的map,挂载action、mutation、getter; installModule(this, state, [], this._modules.root) // 经过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed resetStoreVM(this, state) // 使用插件 plugins.forEach(plugin => plugin(this)) if (Vue.config.devtools) { devtoolPlugin(this) }
能够看出整个构造函数中,主要就是声明一些基础的变量,而后最主要的就是执行了intsllModule函数来注册Module和resetStoreVM来使Store具备“响应式”。
至于ModuleCollection相关的代码咱们暂且不去深究,知道他就是一个Module的收集器,而且提供了一些方法便可。
接下来看这两个主要的方法,首先是installModule,在这个方法中回去生成命名空间,而后挂载mutation、action、getter:
源码位置:/src/store.js
function installModule (store, rootState, path, module, hot) { const isRoot = !path.length const namespace = store._modules.getNamespace(path) // 生成name 和 Module 的 Map if (module.namespaced) { store._modulesNamespaceMap[namespace] = module } if (!isRoot && !hot) { const parentState = getNestedState(rootState, path.slice(0, -1)) const moduleName = path[path.length - 1] // 为module注册响应式; store._withCommit(() => { Vue.set(parentState, moduleName, module.state) }) } const local = module.context = makeLocalContext(store, namespace, path) // 挂载mutation module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) // 挂载action module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) }) // 挂载getter module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) // 递归安装Module module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) }) } // ... // 注册mutation function registerMutation (store, type, handler, local) { // 在_mutations中找到对应type的mutation数组 // 若是是第一次建立,就初始化为一个空数组 const entry = store._mutations[type] || (store._mutations[type] = []) // push一个带有payload参数的包装过的函数 entry.push(function wrappedMutationHandler (payload) { handler.call(store, local.state, payload) }) } // 注册action function registerAction (store, type, handler, local) { // 根据type找到对应的action; const entry = store._actions[type] || (store._actions[type] = []) // push一个带有payload参数的包装过的函数 entry.push(function wrappedActionHandler (payload, cb) { let res = handler.call(store, { dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload, cb) // 若是 res 不是 promise 对象 ,将其转化为promise对象 // 这是由于store.dispatch 方法里的 Promise.all()方法。 if (!isPromise(res)) { res = Promise.resolve(res) } if (store._devtoolHook) { return res.catch(err => { store._devtoolHook.emit('vuex:error', err) throw err }) } else { return res } }) } // 注册getter function registerGetter (store, type, rawGetter, local) { if (store._wrappedGetters[type]) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] duplicate getter key: ${type}`) } return } // 将定义的getter所有存储到_wrappedGetters中; store._wrappedGetters[type] = function wrappedGetter (store) { return rawGetter( local.state, // local state local.getters, // local getters store.state, // root state store.getters // root getters ) } }
在Vuex的module中,咱们是能够拆分不少个module出来的,每个拆分出来的module又能够看成一个全新的module挂载在父级module上,所以这时候就须要一个path变量来区分层级关系了,咱们能够根据这个path来去拿到每一次module下的state、mutation、action等。
接下来是resetStoreVM这个方法,在这个方法中,为store绑定了一个指向新的Vue实例的_vm属性,同时传入了state和computed,computed就是咱们在store中设置的getter。
function resetStoreVM (store, state, hot) { const oldVm = store._vm // bind store public getters store.getters = {} const wrappedGetters = store._wrappedGetters const computed = {} // 为每个getter设置get; forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism computed[key] = () => fn(store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) }) // use a Vue instance to store the state tree // suppress warnings just in case the user has added // some funky global mixins const silent = Vue.config.silent Vue.config.silent = true // 为store绑定Vue实例并注册state和computed store._vm = new Vue({ data: { $$state: state }, computed }) Vue.config.silent = silent // enable strict mode for new vm if (store.strict) { enableStrictMode(store) } // 去除绑定旧vm if (oldVm) { if (hot) { // dispatch changes in all subscribed watchers // to force getter re-evaluation for hot reloading. store._withCommit(() => { oldVm._data.$$state = null }) } Vue.nextTick(() => oldVm.$destroy()) } }
在Vuex中有两个重要的操做,一个是dispatch,一个是commit,咱们经过dispatch去触发一个action,而后在action中咱们经过提交commit去达到更新state的目的。下面就来看看这两部门的源码。
源码位置:/src/store.js
commit (_type, _payload, _options) { // check object-style commit // 检验类型; const { type, payload, options } = unifyObjectStyle(_type, _payload, _options) const mutation = { type, payload } // 找到type对应的mutation方法; const entry = this._mutations[type] if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown mutation type: ${type}`) } return } // 执行mutation; this._withCommit(() => { entry.forEach(function commitIterator (handler) { handler(payload) }) }) // 通知订阅者 this._subscribers.forEach(sub => sub(mutation, this.state)) if ( process.env.NODE_ENV !== 'production' && options && options.silent ) { console.warn( `[vuex] mutation type: ${type}. Silent option has been removed. ` + 'Use the filter functionality in the vue-devtools' ) } } dispatch (_type, _payload) { // check object-style dispatch // 检验值; const { type, payload } = unifyObjectStyle(_type, _payload) const action = { type, payload } // 获取type对应的action; const entry = this._actions[type] if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown action type: ${type}`) } return } // 通知action订阅者; this._actionSubscribers.forEach(sub => sub(action, this.state)) // 返回action return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload) }
Vuex为咱们提供了一些静态方法,都是经过调用绑定在Vue实例上的Store实例来操做咱们的state、mutation、action和getter等。
源码位置:/src/helpers.js
//返回一个对象 //对象的属性名对应于传入的 states 的属性名或者数组元素 //执行这个函数的返回值根据 val 的不一样而不一样 export const mapState = normalizeNamespace((namespace, states) => { const res = {} normalizeMap(states).forEach(({ key, val }) => { res[key] = function mappedState () { let state = this.$store.state let getters = this.$store.getters if (namespace) { 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] } // mark vuex getter for devtools res[key].vuex = true }) return res }) // 返回一个对象 // 执行这个函数后将触发指定的 mutation export const mapMutations = normalizeNamespace((namespace, mutations) => { const res = {} normalizeMap(mutations).forEach(({ key, val }) => { res[key] = function mappedMutation (...args) { // Get the commit method from store let commit = this.$store.commit if (namespace) { const module = getModuleByNamespace(this.$store, 'mapMutations', namespace) if (!module) { return } commit = module.context.commit } return typeof val === 'function' ? val.apply(this, [commit].concat(args)) : commit.apply(this.$store, [val].concat(args)) } }) return res }) export const mapGetters = normalizeNamespace((namespace, getters) => { const res = {} normalizeMap(getters).forEach(({ key, val }) => { // thie namespace has been mutate by normalizeNamespace val = namespace + val res[key] = function mappedGetter () { if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) { return } if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) { console.error(`[vuex] unknown getter: ${val}`) return } return this.$store.getters[val] } // mark vuex getter for devtools res[key].vuex = true }) return res }) export const mapActions = normalizeNamespace((namespace, actions) => { const res = {} 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 }) // 接受一个对象或者数组,最后都转化成一个数组形式,数组元素是包含key和value两个属性的对象 function normalizeMap (map) { return Array.isArray(map) ? map.map(key => ({ key, val: key })) : Object.keys(map).map(key => ({ key, val: map[key] })) } function normalizeNamespace (fn) { return (namespace, map) => { if (typeof namespace !== 'string') { map = namespace namespace = '' } else if (namespace.charAt(namespace.length - 1) !== '/') { namespace += '/' } return fn(namespace, map) } }
笔者没有将所有的源码贴出来逐行分析,只是简单的分析了核心逻辑的源码。总的来讲Vuex源码很少,写的很精练也很易懂,但愿你们都能抽时间亲自看看源码学习学习。