1、Vuex 20 源码分析上Vuex 2.0 源码分析(上)既然 Vue.js 2.0 已经正式发布了,我们也要紧跟步伐,和大家聊一聊 Vuex 2.0。本文并不打算讲官网已有的内容,而会通过源码分析的方式,让同学们从另外一个角度认识和理解 Vuex 2.0。当我们用 Vue.js 开发一个中到大型的单页应用时,经常会遇到如下问题: 如何让多个 Vue 组件共享状态 Vue 组件间如何通讯通常,在项目不是很复杂的时候,我们会利用全局事件总线 (global event bus)解决,但是随着复杂度的提升,这些代码将变的难以维护。因此,我们需要一种更加好用的解决方案,于是,Vuex 诞生了。本文并
2、不是 Vuex 的科普文章,对于还不了解 Vuex 的同学,建议先移步Vuex 官方文档;看英文文档吃力的同学,可以看Vuex 的中文文档。Vuex 的设计思想受到了 Flux,Redux 和 The Elm Architecture 的启发,它的实现又十分巧妙,和 Vue.js 配合相得益彰,下面就让我们一起来看它的实现吧。目录结构Vuex 的源码托管在github,我们首先通过 git 把代码 clone 到本地,选一款适合自己的 IDE 打开源码,展开 src 目录,如下图所示:src 目录下的文件并不多,包含几个 js 文件和 plugins 目录, plugins 目录里面包含 2
3、个 Vuex 的内置插件,整个源码加起来不过 500-600 行,可谓非常轻巧的一个库。麻雀虽小,五脏俱全,我们先直观的感受一下源码的结构,接下来看一下其中的实现细节。源码分析本文的源码分析过程不会是自上而下的给代码加注释,我更倾向于是从 Vuex 提供的 API 和我们的使用方法等维度去分析。Vuex 的源码是基于 es6 的语法编写的,对于不了解 es6 的同学,建议还是先学习一下 es6。从入口开始看源码一般是从入口开始,Vuex 源码的入口是 src/index.js,先来打开这个文件。我们首先看这个库的 export ,在 index.js 代码最后。exportdefaultSto
4、re, install, mapState, mapMutations, mapGetters, mapActions这里可以一目了然地看到 Vuex 对外暴露的 API。其中, Store 是 Vuex 提供的状态存储类,通常我们使用 Vuex 就是通过创建 Store 的实例,稍后我们会详细介绍。接着是 install 方法,这个方法通常是我们编写第三方 Vue 插件的“套路”,先来看一下“套路”代码:function install (_Vue)if(Vue) console.error(vuex already installed. Vue.use(Vuex) should be ca
5、lled only once.)returnVue=_Vue applyMixin(Vue)/ auto install in dist modeif(typeof window !=undefined& window.Vue) install(window.Vue)我们实现了一个 install 方法,这个方法当我们全局引用 Vue ,也就是 window 上有 Vue 对象的时候,会手动调用 install 方法,并传入 Vue 的引用;当 Vue 通过 npm 安装到项目中的时候,我们在代码中引入第三方 Vue 插件通常会编写如下代码:importVuefromvueimportVuex
6、fromvuex.Vue.use(Vuex)当我们执行 Vue.use(Vuex) 这句代码的时候,实际上就是调用了 install 的方法并传入 Vue 的引用。install 方法顾名思义,现在让我们来看看它的实现。它接受了一个参数 _Vue,函数体首先判断 Vue ,这个变量的定义在 index.js 文件的开头部分:let Vue/ bind on install对 Vue 的判断主要是保证 install 方法只执行一次,这里把 install 方法的参数 _Vue 对象赋值给 Vue 变量,这样我们就可以在 index.js 文件的其它地方使用 Vue 这个变量了。install
7、方法的最后调用了 applyMixin 方法,我们顺便来看一下这个方法的实现,在 src/mixin.js 文件里定义:export default function (Vue) const version = Number(Vue.version.split(.)0) if (version = 2) const usesInit = Vue.config._lifecycleHooks.indexOf(init) -1 Vue.mixin(usesInit ? init: vuexInit : beforeCreate: vuexInit ) else / override init an
8、d inject vuex init procedure / for 1.x backwards compatibility. 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
9、list. */ function vuexInit () const options = this.$options / store injection if (options.store) this.$store = options.store else if (options.parent & options.parent.$store) this.$store = options.parent.$store 这段代码的作用就是在 Vue 的生命周期中的初始化(1.0 版本是 init,2.0 版本是 beforeCreated)钩子前插入一段 Vuex 初始化代码。这里做的事情很简单给
10、 Vue 的实例注入一个$store的属性,这也就是为什么我们在 Vue 的组件中可以通过this.$store.xxx访问到 Vuex 的各种数据和状态。认识 Store 构造函数我们在使用 Vuex 的时候,通常会实例化 Store 类,然后传入一个对象,包括我们定义好的 actions、getters、mutations、state等,甚至当我们有多个子模块的时候,我们可以添加一个 modules 对象。那么实例化的时候,到底做了哪些事情呢?带着这个疑问,让我们回到 index.js 文件,重点看一下 Store 类的定义。Store 类定义的代码略长,我不会一下就贴上所有代码,我们来拆
11、解分析它,首先看一下构造函数的实现:class Store constructor (options = ) assert(Vue, must call Vue.use(Vuex) before creating a store instance.) assert(typeof Promise != undefined, vuex requires a Promise polyfill in this browser.) const state = , plugins = , strict = false = options / store internal state this._optio
12、ns = options this._committing = false this._actions = Object.create(null) this._mutations = Object.create(null) this._wrappedGetters = Object.create(null) this._runtimeModules = Object.create(null) this._subscribers = this._watcherVM = new Vue() / bind commit and dispatch to self const store = this
13、const dispatch, commit = this this.dispatch = function boundDispatch (type, payload) return dispatch.call(store, type, payload) mit = function boundCommit (type, payload, options) return commit.call(store, type, payload, options) / strict mode this.strict = strict / init root module. / this also rec
14、ursively registers all sub-modules / and collects all module getters inside this._wrappedGetters installModule(this, state, , options) / initialize the store vm, which is responsible for the reactivity / (also registers _wrappedGetters as computed properties) resetStoreVM(this, state) / apply plugin
15、s plugins.concat(devtoolPlugin).forEach(plugin = plugin(this) . 构造函数的一开始就用了“断言函数”,来判断是否满足一些条件。assert(Vue,must call Vue.use(Vuex) before creating a store instance.)这行代码的目的是确保 Vue 的存在,也就是在我们实例化 Store 之前,必须要保证之前的 install 方法已经执行了。assert(typeofPromise!=undefined,vuex requires a Promise polyfill in this b
16、rowser.)这行代码的目的是为了确保 Promsie 可以使用的,因为 Vuex 的源码是依赖 Promise 的。Promise 是 es6 提供新的 API,由于现在的浏览器并不是都支持 es6 语法的,所以通常我们会用 babel 编译我们的代码,如果想使用 Promise 这个 特性,我们需要在 package.json 中添加对 babel-polyfill 的依赖并在代码的入口加上import babel-polyfill这段代码。再来看看 assert 这个函数,它并不是浏览器原生支持的,它的实现在 src/util.js 里,代码如下:exportfunctionasser
17、t(condition, msg)if(!condition)thrownewError(vuex $msg)非常简单,对 condition 判断,如果不不为真,则抛出异常。这个函数虽然简单,但这种编程方式值得我们学习。再来看构造函数接下来的代码:const state =, plugins =, strict =false= options这里就是利用 es6 的结构赋值拿到 options 里的 state,plugins 和 strict。state 表示 rootState,plugins 表示应用的插件、strict 表示是否开启严格模式。接着往下看:/ store interna
18、l statethis._options = optionsthis._committing =falsethis._actions =Object.create(null)this._mutations =Object.create(null)this._wrappedGetters =Object.create(null)this._runtimeModules =Object.create(null)this._subscribers =this._watcherVM =newVue()这里主要是创建一些内部的属性:this._options存储参数 options。this._comm
19、itting标志一个提交状态,作用是保证对 Vuex 中 state 的修改只能在 mutation 的回调函数中,而不能在外部随意修改 state。this._actions用来存储用户定义的所有的 actions。this._mutations用来存储用户定义所有的 mutatins。this._wrappedGetters用来存储用户定义的所有 getters 。this._runtimeModules用来存储所有的运行时的 modules。this._subscribers用来存储所有对 mutation 变化的订阅者。this._watcherVM是一个 Vue 对象的实例,主要是利
20、用 Vue 实例方法 $watch 来观测变化的。继续往下看:/ bind commit and dispatch to selfconst store =thisconst dispatch, commit =thisthis.dispatch =function boundDispatch (type, payload)return dispatch.call(store, type, payload)mit =function boundCommit (type, payload, options)return commit.call(store, type, payload, opti
21、ons)/ strict modethis.strict = strict这里的代码也不难理解,把 Store 类的 dispatch 和 commit 的方法的 this 指针指向当前 store 的实例上,dispatch 和 commit 的实现我们稍后会分析。this.strict 表示是否开启严格模式,在严格模式下会观测所有的 state 的变化,建议在开发环境时开启严格模式,线上环境要关闭严格模式,否则会有一定的性能开销。Vuex 的初始化核心installModule我们接着往下看:/ init root module./ this also recursively regist
22、ers all sub-modules/ and collects all module getters inside this._wrappedGettersinstallModule(this, state, options)/ initialize the store vm, which is responsible for the reactivity/ (also registers _wrappedGetters as computed properties)resetStoreVM(this, state)/ apply pluginsplugins.concat(devtool
23、Plugin).forEach(plugin = plugin(this)这段代码是 Vuex 的初始化的核心,其中,installModule 方法是把我们通过 options 传入的各种属性模块注册和安装;resetStoreVM 方法是初始化 store._vm,观测 state 和 getters 的变化;最后是应用传入的插件。下面,我们先来看一下 installModule 的实现:function installModule (store, rootState, path, module, hot) const isRoot = !path.length const state,
24、actions, mutations, getters, modules = module / set state if (!isRoot & !hot) const parentState = getNestedState(rootState, path.slice(0, -1) const moduleName = pathpath.length - 1 store._withCommit() = Vue.set(parentState, moduleName, state | ) ) if (mutations) Object.keys(mutations).forEach(key =
25、registerMutation(store, key, mutationskey, path) ) if (actions) Object.keys(actions).forEach(key = registerAction(store, key, actionskey, path) ) if (getters) wrapGetters(store, getters, path) if (modules) Object.keys(modules).forEach(key = installModule(store, rootState, path.concat(key), moduleske
26、y, hot) ) installModule 函数可接收5个参数,store、rootState、path、module、hot,store 表示当前 Store 实例,rootState 表示根 state,path 表示当前嵌套模块的路径数组,module 表示当前安装的模块,hot 当动态改变 modules 或者热更新的时候为 true。先来看这部分代码:const isRoot =!path.lengthconst state, actions, mutations, getters, modules=module代码首先通过 path 数组的长度判断是否为根。我们在构造函数调用的
27、时候是installModule(this, state, , options),所以这里 isRoot 为 true。module 为传入的 options,我们拿到了 module 下的 state、actions、mutations、getters 以及嵌套的 modules。接着看下面的代码:/ set stateif(!isRoot &!hot)const parentState = getNestedState(rootState, path.slice(0,-1)const moduleName = pathpath.length -1 store._withCommit()=V
28、ue.set(parentState, moduleName, state |)这里判断当不为根且非热更新的情况,然后设置级联状态,这里乍一看不好理解,我们先放一放,稍后来回顾。再往下看代码:if(mutations)Object.keys(mutations).forEach(key = registerMutation(store, key, mutationskey, path)if(actions)Object.keys(actions).forEach(key = registerAction(store, key, actionskey, path)if(getters) wrap
29、Getters(store, getters, path)这里分别是对 mutations、actions、getters 进行注册,如果我们实例化 Store 的时候通过 options 传入这些对象,那么会分别进行注册,我稍后再去介绍注册的具体实现。那么到这,如果 Vuex 没有 module ,这个 installModule 方法可以说已经做完了。但是 Vuex 巧妙了设计了 module 这个概念,因为 Vuex 本身是单一状态树,应用的所有状态都包含在一个大对象内,随着我们应用规模的不断增长,这个 Store 变得非常臃肿。为了解决这个问题,Vuex 允许我们把 store 分 m
30、odule(模块)。每一个模块包含各自的 state、mutations、actions 和 getters,甚至是嵌套模块。所以,接下来还有一行代码:if(modules)Object.keys(modules).forEach(key = installModule(store, rootState, path.concat(key), moduleskey, hot)这里通过遍历 modules,递归调用 installModule 去安装子模块。这里传入了 store、rootState、path.concat(key)、和 moduleskey,和刚才不同的是,path 不为空,module 对应为子模块,那么我们回到刚才那段代码:/ set stateif(!isRoot &!hot)const parentState = getNestedState(rootState, path.slice(0,-1)const moduleName = pathpath.length -1 store._withCommit()=Vue.set(parentState, module
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1