vue源码解读(三)Vuex源码解析:Store构造类详解
2020/1/2 14:05:24
本文主要是介绍vue源码解读(三)Vuex源码解析:Store构造类详解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
欢迎star我的github仓库,共同学习~目前vue源码学习系列已经更新了6篇啦~
https://github.com/yisha0307/...
快速跳转:
- Vue的双向绑定原理(已完成)
- 说说vue中的Virtual DOM(已完成)
- React diff和Vue diff实现差别
- Vue中的异步更新策略(已完成)
- Vuex的实现理解(已完成)
- Typescript学习笔记(持续更新ing)
- Vue源码中闭包的使用(已完成)
Vuex
首先,vuex是什么?
根据官方的解释,
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
vue的整个状态管理主要包括以下几部分:
- state: 驱动应用的数据源;
- view: 以声明方式将 state 映射到视图;
- actions: 响应在 view 上的用户输入导致的状态变化。
看一下官网提供的单向数据流示意:
但是,如果我们要在兄弟组件之间传值,或者要多个组件共享一个状态,这个单向数据流就会遭到破坏。在不使用vuex的时候,需要在父组件中定义好state,再用props传递给子组件,如果子组件要修改父组件的状态,就需要使用$emit
事件。稍微进阶一点,可以使用busEvent, 就是new一个Vue实例出来,通过在target component上$on
注册上一个事件,在source component上$emit触发这个事件,引起target component的状态改变。但是,一旦应用变得大型,这两种方式就变得非常脆弱。所以,vuex就诞生啦~
还是看一下官网的vuex示意图:
Vuex实现了一个单项数据流,在全局拥有一个state存放数据,所有修改state的操作必须通过mutation来进行执行,同时mutation只能同步修改state,如果要异步修改就需要调用action, 而action内部也是要使用mutation进行修改state的操作。最后,根据state的变化,渲染到视图上。
Vuex运行依赖Vue内部双向绑定机制,需要new Vue的实例来实现,因此,vuex只能和vue搭配使用。
更多vuex的详细介绍请参阅Vuex官方教程。
this.$store注入
首先,看一下store是如何install到vue上的,以下是vuex的install代码:
/*暴露给外部的插件install方法,供Vue.use调用安装插件*/ export function install (_Vue) { if (Vue) { /*避免重复安装(Vue.use内部也会检测一次是否重复安装同一个插件)*/ if (process.env.NODE_ENV !== 'production') { console.error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ) } return } /*保存Vue,同时用于检测是否重复安装*/ Vue = _Vue applyMixin(Vue) }
这段代码主要做了两件事:
- 检查有没有重复安装vuex
- 如果没有安装的话,就去调用applyMixin()
接下来看一下applyMixin
的代码:
export default function (Vue) { /*获取Vue版本,鉴别Vue1.0还是Vue2.0*/ const version = Number(Vue.version.split('.')[0]) if (version >= 2) { /*通过mixin将vuexInit混淆到Vue实例的beforeCreate钩子中*/ Vue.mixin({ beforeCreate: vuexInit }) } else { // override init and inject vuex init procedure // for 1.x backwards compatibility. /*将vuexInit放入_init中调用*/ const _init = Vue.prototype._init Vue.prototype._init = function (options = {}) { options.init = options.init ? [vuexInit].concat(options.init) : vuexInit _init.call(this, options) } } /** * Vuex init hook, injected into each instances init hooks list. */ /*Vuex的init钩子,会存入每一个Vue实例等钩子列表*/ function vuexInit () { const options = this.$options // store injection if (options.store) { /*存在store其实代表的就是Root节点,直接执行store(function时)或者使用store(非function)*/ this.$store = typeof options.store === 'function' ? options.store() : options.store } else if (options.parent && options.parent.$store) { // 从父组件里获取this.$store this.$store = options.parent.$store } } }
上面的代码解释一下,就是先去检查vue的版本,如果是2.0就在beforeCreate的时候调用VueInit这个方法,如果是1.0就放入Vue的_init中;
然后再看一下vuexInit这个方法,如果options存在store的话就证明是Root节点,直接执行store(因为我们注入store的时候,都会在new Vue的时候放进去,比如:
new Vue({ el: '#app', store })
所以如果options里有store,就直接执行即可;如果没有,就从父组件中获取$store,这样就保证了所有组件都公用了全局的同一份store。
通过以上步骤,就完成了this.$store的注入,在工程的任何地方都可以应用,而且指向的都是同一个store。
Store
回想一下,我们在定义Store的时候,通常是这样写的 (来自vuex官方教程举例):
const moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: { ... }, mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { moduleA, moduleB } })
因此,初始化store实例的时候就注册了modules里的state/mutations/getters/actions。来看一下Store的代码(因为store构造类的源码很多,会一部分一部分讲):
/*Store构造类*/ export class Store { constructor (options = {}) { // 首先会检查一下浏览器环境内有没有vue if (!Vue && typeof window !== 'undefined' && window.Vue) { install(window.Vue) } const { plugins = [], // 默认严格模式开启 strict = false } = options /*从option中取出state,如果state是function则执行,最终得到一个对象*/ let { state = {} } = options if (typeof state === 'function') { state = state() } // store internal state /* 用来判断严格模式下是否是用mutation修改state的 */ this._committing = false // 以下是初始化actions/mutations/wrapperGetters/modules等 /* 存放action */ this._actions = Object.create(null) /* 存放mutation */ this._mutations = Object.create(null) /* 存放getter */ this._wrappedGetters = Object.create(null) /* module收集器 */ this._modules = new ModuleCollection(options) /* 根据namespace存放module */ this._modulesNamespaceMap = Object.create(null) /* 存放订阅者 */ this._subscribers = [] /* 用以实现Watch的Vue实例 */ this._watcherVM = new Vue() // bind commit and dispatch to self /*将dispatch与commit调用的this绑定为store对象本身,否则在组件内部this.dispatch时的this会指向组件的vm*/ const store = this const { dispatch, commit } = this /* 为dispatch与commit绑定this(Store实例本身) */ // dispatch对应actions, commit对应mutations (dispatch里会有promise实现) 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 // 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.forEach(plugin => plugin(this)) /* devtool插件 */ if (Vue.config.devtools) { devtoolPlugin(this) } }
以上代码是Vue构造类的第一部分,可以看到除了初始化actions/mutations/modules...等等,还调用了installModule和resetStoreVM这两个方法。
看一下这两个方法分别是什么:
function installModule (store, rootState, path, module, hot) { const isRoot = !path.length // path是[]表示是根节点 /* 获取module的namespace */ const namespace = store._modules.getNamespace(path) // register in namespace map /* 如果有namespace则在_modulesNamespaceMap中注册 */ if (module.namespaced) { store._modulesNamespaceMap[namespace] = module } // set state if (!isRoot && !hot) { /* 获取父级的state */ const parentState = getNestedState(rootState, path.slice(0, -1)) /* module的name */ const moduleName = path[path.length - 1] // 这边也用_withCommit是因为state只能在_committing === true的时候进行修改 store._withCommit(() => { // Vue.set内部会用defineReactive将module.state设置成响应式的 // defineReactive的代码详见第一节 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 namespacedType = namespace + key registerAction(store, namespacedType, action, local) }) /* 遍历注册getter */ module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) /* 递归安装mudule */ module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) }) }
installModule的作用主要是为module加上namespace名字空间(如果有)后,注册mutation、action以及getter,同时递归安装所有子module。
/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */ function resetStoreVM (store, state, hot) { /* 存放之前的vm对象 */ const oldVm = store._vm // bind store public getters store.getters = {} const wrappedGetters = store._wrappedGetters const computed = {} /* 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的computed属性 */ 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的目的是在new一个Vue实例的过程中不会报出一切警告 */ Vue.config.silent = true /* 这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/ store._vm = new Vue({ data: { $$state: state }, computed }) Vue.config.silent = silent // enable strict mode for new vm /* 使能严格模式,保证修改store只能通过mutation */ if (store.strict) { enableStrictMode(store) } if (oldVm) { /* 解除旧vm的state的引用,以及销毁旧的Vue对象 */ 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()) } }
在这段代码中, 其实是利用了vue.js可以对数据进行双向绑定的特点(具体参见Vue的双向绑定原理),new了一个vue的实例,并且绑定了state和computed, 这样就可以实现data和视图的同步更新。
继续看下Vuex.store里还有什么:
get state () { return this._vm._data.$$state } set state (v) { // state不允许通过set进行修改,必须通过commit if (process.env.NODE_ENV !== 'production') { assert(false, `Use store.replaceState() to explicit replace store state.`) } }
get没什么可说的,就是返回这个state里的值,但是set这边除了一个断言就没有了,意思是state里的数据并不能直接修改,必须用mutation。
Commit && Dispatch
再看一下store里提供的比较重要的两个api —— commit 和dispatch。
commit (_type, _payload, _options) { // unifyObjectStyle主要是统一参数的格式 // 因为mutation 支持两种写法 (一种是列举参数,一种是把参数放在{}里,具体参见教程) 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' ) } } _withCommit (fn) { // _committing是一个标志位,在strict模式下保证只能通过mutation来修改store的数据 const committing = this._committing this._committing = true fn() this._committing = committing }
mutation用的这个commit方法,要注意的就是这个_withCommit,在_withCommit里_committing是true, 因此,_committing是一个标志位,如果在strice mode(default true)下,一定要用commit方法才能修改。看一下这个断言:
function enableStrictMode (store) { store._vm.$watch(function () { return this._data.$$state }, () => { if (process.env.NODE_ENV !== 'production') { // function assert (condition, msg) { // if (!condition) throw new Error(`[vuex] ${msg}`) // } assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`) } }, { deep: true, sync: true }) }
调用的vue的watch方法,在有变动的时候做一个检验,如果_committing不是true, 就扔出error。
继续看给actions调用的dispatch方法:
/* 调用action的dispatch方法 */ dispatch (_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_type, _payload) /* actions中取出type对应的ation */ const entry = this._actions[type] if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown action type: ${type}`) } return } /* 是数组则包装Promise形成一个新的Promise,只有一个则直接返回第0个 */ return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload) }
dispatch
这个方法比较简单,使用了Promise.all, 所以可以执行异步操作。
但是我们在实际操作mutation和action的时候,是可以获取到state和调用commit的,这是怎么做到的呢?看一下registerMutation
和registerAction
两个方法。
/* 遍历注册mutation */ function registerMutation (store, type, handler, local) { /* 所有的mutation会被push进一个数组中,这样相同的mutation就可以调用不同module中的同名的mutation了 */ const entry = store._mutations[type] || (store._mutations[type] = []) 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] = []) 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) /* 判断是否是Promise */ if (!isPromise(res)) { /* 不是Promise对象的时候转化称Promise对象 */ res = Promise.resolve(res) } if (store._devtoolHook) { /* 存在devtool捕获的时候触发vuex的error给devtool */ return res.catch(err => { store._devtoolHook.emit('vuex:error', err) throw err }) } else { return res } }) }
可以看到,在registerMutation
里,push进mutationHandler的时候,会塞进local.state, 而在registerAction
里,push进actionHandler的时候,第一个参数会包装成dispatch/commit/getters/state...的对象,这样在action里就可以去调用这些属性和方法。
于是,我们就可以这样使用mutation和action:
// vuex里定义mutation: increment (state, payload) { state.count += payload.amount } // vuex里定义action: (使用解构) actionB ({ dispatch, commit }) { return dispatch('actionA').then(() => { commit('someOtherMutation') }) }
store提供的其他api
最后一口气把store里的代码都看完吧:
/* 观察一个getter方法 */ 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) } /* 重置state */ replaceState (state) { this._withCommit(() => { this._vm._data.$$state = state }) }
- watch这个方法比较有趣的点在于,_watcherVM在store的构造函数里被定义成new Vue(), 因此可以直接采用vue的$watch去监听getter里的值的变化。
- replaceState就比较简单,替换了state的根状态。
/* 注册一个动态module,当业务进行异步加载的时候,可以通过该接口进行注册动态module */ registerModule (path, rawModule) { /* 转化称Array */ if (typeof path === 'string') path = [path] if (process.env.NODE_ENV !== 'production') { assert(Array.isArray(path), `module path must be a string or an Array.`) assert(path.length > 0, 'cannot register the root module by using registerModule.') } /*注册*/ this._modules.register(path, rawModule) /*初始化module*/ installModule(this, this.state, path, this._modules.get(path)) // reset store to update getters... /* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */ resetStoreVM(this, this.state) } /* 注销一个动态module */ unregisterModule (path) { /* 转化称Array */ if (typeof path === 'string') path = [path] if (process.env.NODE_ENV !== 'production') { assert(Array.isArray(path), `module path must be a string or an Array.`) } /*注销*/ this._modules.unregister(path) this._withCommit(() => { /* 获取父级的state */ const parentState = getNestedState(this.state, path.slice(0, -1)) /* 从父级中删除 */ Vue.delete(parentState, path[path.length - 1]) }) /* 重制store */ resetStore(this) }
先看一下registerModule
这个api的调用方式:
// 注册myModule store.registerModule('myModule', { // ... }) // 注册嵌套模块 `nested/myModule` store.registerModule(['nested', 'myModule'], { // ... })
所以,在调用registerModule
的时候,因为可以传string和array,所以在一开始要统一成array;之后再调用installModule
和resetStoreVM
这两个Vuex.store里最重要的方法,初始化module并重设store。
而unregisterModule
的作用就是注销掉一个动态模块。调用的方式就是unregisterModule(path: string | Array<string>)
,在内部首先和registerModule
一样,把string统一成array, 如果不是这两种数据结构的参数,就抛错出来~之后在moudles里unregister这个path, 通过commit的方式把该module里的state注销掉,同时重置store。
结语
store的代码基本就讲完啦,可以看到,vuex的代码其实很多都是高度依赖vue自身支持双向绑定的特性,比如store构造函数里的resetStoreVM()
, 就是new了一个Vue的实例,运用vue内部的响应式注册了state和computed;再比如,store提供的api-watch
也是使用的vm.$watch。因此,vuex只能和vue搭配使用,不能做其他框架的状态管理。
这篇关于vue源码解读(三)Vuex源码解析:Store构造类详解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-05-09一定要避坑:关于微信H5分享,温馨提示你不要再踩坑了!!!
- 2024-05-09本地项目放到公网访问!炒鸡煎蛋!
- 2024-04-07金融企业区域集中库的设计构想和测试验证
- 2024-03-11前端CSS的工程化——掌握Sass这四大特性就够了
- 2024-02-21h5关联css样式(html怎么和css关联)-icode9专业技术文章分享
- 2024-02-07Firefox禁止远程字体加速网页加载及图标字体补充安装
- 2024-02-07一个菜鸡前端的3年总结-「2023」
- 2024-01-18最火前端Web组态软件(可视化)
- 2024-01-12程序员提效 x10 的必备开源“神器”
- 2024-01-11前端可以监控静态资源的时间吗-icode9专业技术文章分享