zero-blog
zero-blog copied to clipboard
[email protected] 源码分析
[email protected] 源码分析
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
如果您还不熟悉 Vuex 的使用,请先去过一遍 Vuex 的 api ,跟着官方的 demo 走一遍,了解了 Vuex 的神奇之后,带着问题来一起看源码。
在进入源码之前,先列出几点对于Vuex
的问题:
-
action,mutation,getter 的回调函数里面的是怎么放入一个Store实例相关对象的(包含:dispatch,commit,state,rootState,getters,rooteGetters等)。
-
registerModule 和 unregisterModule 。
-
state 和 getter 是不是通过 new Vue() 来使其变为响应式的。
-
我们不能修改 state 是怎么做到的。
下面我们带着这几个问题来进入源码。
进入源码
分析源码使用的是 Vuex 2.3.1版本,建议 clone 下代码,使用本文提供的例子,跟着一起走,能加快理解。
目录结构
-
module-collection.js:按照传入的选项,进行 module 收集,建立 module 之间的关系;
-
module.js:生成一个 module 对象 包含私有属性和方法;
-
devtool.js:chrome 插件相关;
-
logger.js:订阅相关操作日志;
-
helpers.js:工具函数,mapState,mapMutations,mapGetters,mapActions;
-
index.js:入口文件;
-
mixin.js:在Vue实例上注册 options.store;
-
store.js:提供 store 的各个 module 构建安装;
-
util.js:提供了工具方法如 find、deepCopy、forEachValue 以及 assert 等方法;
入口开始
//index.js 入口文件
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions } from './helpers'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions
}
入口文件很简单,是vuex对外暴露的API:
-
Store:从我们 new Vuex.Store() 这样的方式,我们就该知道的是 Store 就是为我们生成状态树,并提供操作方法,定义操作规则的类;
-
install:Vue 插件安装,通过 Vue.use(Vuex) 执行,顺便看下 Vue.use 的代码;
-
mapState,mapMutations,mapGetters,mapActions 都是方便我们操作的函数。
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
/* istanbul ignore if */
if (plugin.installed) {
return
}
// additional parameters
const args = toArray(arguments, 1)
//参数首位放入 Vue
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
plugin.installed = true
return this
}
}
plugin.install.apply(plugin, args) 调用了插件的 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'
let Vue
class Store {...}
//被调用 Vue 被传进来
export function install (_Vue) {
if (Vue) {
//Vue.use(Vuex) 第二次就会报错 通过保存 Vue 为变量来进行判断
//在调用 Vue 内部的方法时,减少作用域链查找
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
return
}
Vue = _Vue
applyMixin(Vue) //挂载方法和变量
}
// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
不难看出,当 Vue 执行了 install 函数,把 _Vue 作为参数传了进来,全局的 Vue 才有值,在用 Vue.use 便直接报错。
applyMixin:
//mixins.js
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
//vue 2.0以上 使用 Vue 自带的 mixin 方法 在实例的 init 或者 beforeCreate上挂载 vuexInit
const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
} else {
// vue 1.x 也做了相应的兼容 可以看出来没有 mixin 注册起来会很蛋疼 重写 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 初始化钩子
// 每次通过 new vue(),vue.component() 创建新的 Vue 实例时候,都会被注入执行
function vuexInit () {
const options = this.$options
// store 注入
if (options.store) {
// vueInit 步骤①
// this 都指向传入的 Vue
// this.$options.store 和 this.$store 访问的是一个 store
this.$store = options.store
} else if (options.parent && options.parent.$store) {
// vueInit 步骤②
this.$store = options.parent.$store
}
}
}
因为对每个实例和组件执行以下方法,这就带来了一些问题, 我们公共型的组件是不需要 store 的, 但 Vue 其实并无法区分公共型组件和业务型组件,便取了一个折中的方式,都给 store 。
先不用着急理解 vuexInit 方法,我们先回顾下开始是如何初始注入 Store 的。
new Vue({
el: '#root',
router,
store,
render: h => h(App)
})
我们在 Vue 应用的根组件上注入了我们的 Store ,在生命周期执行到 init 或者 beforeCreated 的时候,执行了 vuexInit 步骤①,直接在当前根 Vue 上放了一个对象 store 并代理为 $store 。
我们创建子组件,编译过后其实执行的是 Vue.component() 方法,发现自身开始是没有 store 这个对象的,便“继承”一下父级的 store,通过 vueInit 步骤② $options.parent 来建立父子组件的关系。
这也就是所说的单一状态树,在单页面应用中,所有组件默认访问同一个 Store 实例,这个 Store 实例是会自动向下游组件渗透的
走进 class Store
装载实例,配置 store :
import Vue from 'vue'
import Vuex from 'vuex'
// install Vuex 框架
Vue.use(Vuex)
//我们配置 OPTIONS,注意,module2 的 namespaced 为 true
const OPTIONS = {
// rootModule
state: {
name: 'commonState'
},
actions: {
fetchApi({commit}) {
commit('changeName')
}
},
getters: {
commonNameGetter: state => state.name
},
mutations: {
changeName(state) {
state.name = 'commonStateChanged'
}
},
modules: {
module1: {
state: {
name: 'module1State'
},
actions: {
fetchApi({commit}) {
commit('changeName')
}
},
getters: {
nameGetter: state => state.name
},
mutations: {
changeName(state) {
state.name = 'module1Changed'
}
},
modules: {
module2: {
state: {
name: 'module2State'
},
actions: {
fetchApi({commit}) {
commit('changeName')
}
},
getters: {
nameGetter: state => state.name
},
mutations: {
changeName(state) {
state.name = 'module2Changed'
}
},
namespaced: true
}
}
}
},
strict: process.env.NODE_ENV !== 'production'
}
export default new Vuex.Store(OPTIONS)
在这里先区分一下几个概念:
- options 下的第一层,是根模块( rootModule )。
- rootModule 下有一个孩子**( _children )名字叫 module1** 。
- module1 下有一个孩子**( _children )名字叫 module2** 。
你应该已经看出,我们的实例配置的地方也有几个值得注意的点:
- 三个 module 的 state 都有属性 name 。
- 三个 module 的 actions 里面都有个 fetchApi 的方法。
- 三个 module的 mutations 里面都有个 changeName 的方法。
- module1 和 module2 都有 nameGetter 。
- module2 有 namespaced 为 true 的配置。
很明显,我们想通过这个实例看看 namespaced 的重要性。
因为有new
操作符 Vuex.Store 就是个类,所以我们暴露出去的就是一个 Store 的实例,我们把整个状态树是一次性全部传入 Store 类的,传入的 OPTIONS 也就是下面的 options 。
export class Store {
// 构造函数, 实例化执行
constructor (options = {}) {
// 确保 Vue 存在,new Store 之前,必须 Vue.use(Vuex)
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
// 浏览器 Promise不存在,我们需要通过 package.json 中添加对 babel-polyfill 的依赖并在代码的入口加上 import 'babel-polyfill'
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
// 断言函数代码
// export function assert (condition, msg) { if (!condition) throw new Error(`[vuex] ${msg}`) }
// es6 赋值结构取值
// state 表示 rootState(rootModule的state),plugins 表示应用的插件、strict 表示是否开启严格模式。
const {
state = {},
plugins = [],
strict = false
} = options
if (typeof state === 'function') {
state = state()
}
/***① 添加store内部状态 start***/
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()
/***① end***/
const store = this
const { dispatch, commit } = this
/***② dispatch,commit start***/
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)
}
/***② end***/
/***③ 严格模式 start***/
this.strict = strict
/***③ end***/
/***④ installModule start***/
// 初始化rootModule
// 同时递归的注册所有子 modules ,
// 在 this._wrappedGetters 中收集所有 module 的 getters
installModule(this, state, [], this._modules.root)
/***④ end***/
/***⑤ resetStoreVM start***/
// 在 vm 中初始化响应式的 store
// 同时注册所有的 _wrappedGetters 作为计算属性
resetStoreVM(this, state)
/***⑤ end***/
// 插件注册
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
}
....
....
}
整个constructor
分为五个大块,我们逐一分解。
① 添加 store 内部状态
- this._committing 标志一个提交状态,作用是保证对 Vuex 中 state 的修改只能通过 _withCommit 方法,在 mutation 的回调函数中,而不能在外部随意修改 state。
- this._actions 用来存储用户定义的所有 actions。
- this._mutations 用来存储用户定义所有 mutatins。
- this._wrappedGetters 用来存储用户定义的所有 getters。
- this._modules 用来存储所有运行时 module,并定义好他们的关系。
- this._modulesNamespaceMap 用来存储 options.namespaced 为 true 的 module。
- this._subscribers 用来存储所有对 mutation 变化的订阅者。
- this._watcherVM 是一个 Vue 对象的实例,调用 $watch 方法。
我们注意到 this._modules 是通过 ModuleCollection 类来生成的。
//module-collection.js
export default class ModuleCollection {
// rawRootModule 也就是我们初始配置的 options
constructor (rawRootModule) {
// ModuleCollection 实例的 root 对象存放 Module 实例
// 第一次构建是非运行时,为 false
this.root = new Module(rawRootModule, false)
// rootModule 如果有 modules,注册所有相邻的 modules。
// 根据 options,这里注册的是 module1
if (rawRootModule.modules) {
forEachValue(rawRootModule.modules, (rawModule, key) => {
// key: 'module1', rawModule: module1的值
this.register([key], rawModule, false)
})
}
}
get (path) {
// 从 rootModule 中找到 path 中对应的 module
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
register (path, rawModule, runtime = true) {
const parent = this.get(path.slice(0, -1))
const newModule = new Module(rawModule, runtime)
parent.addChild(path[path.length - 1], newModule)
// 递归注册子 modules
// 根据 options,这里注册 module2
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
....
ModuleCollection
实例包含:
- 属性:root
- 原型方法:get,getNamespace,update (传入一个新 module,递归更新所有 module 的 actions,mutations,getters),register,unregister
进入Module类
来看下constructor
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null)
this._rawModule = rawModule
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
// namespaced 属性是可以传可以不传的,所以默认都返回 false
get namespaced () {
return !!this._rawModule.namespaced
}
...
}
Module
的实例包含:
- 属性:runtime,children,rawModule,rawState,namespaced;
- 原型方法:addChild,removeChild,getChild,update (更新 module 的 _rawModule),forEachChild,forEachGetter,forEachAction,forEachMutation;
最终在 Store 实例的this._modules
结构如下:
总结一下这两个类的作用:
就是构造一棵由 module 实例为节点的树( root 对象),并给这颗树和树的节点分别添加相应的属性和方法。
② dispatch和commit
dispatch (_type, _payload) {
const {
type,
payload
} = unifyObjectStyle(_type, _payload) // 配置参数处理
// 所有的 action 是存在放 Store 内部状态 _actions 中
const entry = this._actions[type]
if (!entry) {
console.error(`[vuex] unknown action type: ${type}`)
return
}
// 写法简便兼顾性能
return entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
}
拿到 _actions 入口的引用entry
。
entry
是个数组,长度等于1时直接执行里面的函数,当拿到的entry
长度大于1,就会遍历按顺序执行。
不妨做个假设,因为 rootModule 和 module1 的 action 名字相同,但他们都没有 namespaced 为 true 的设置,所以在这里可能是在同一个entry
里,mutation 同理。
commit (_type, _payload, _options) {
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
// 所有的 mutation 是存在放 Store 内部状态 _mutations 中
const entry = this._mutations[type]
if (!entry) {
console.error(`[vuex] unknown mutation type: ${type}`)
return
}
// 专用修改 state 方法,下面会做说明
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 订阅者函数遍历执行,传入 mutation,state
// 告诉所有订阅函数本次操作的 type,payload 和当前 state 状态啊。
this._subscribers.forEach(sub => sub(mutation, this.state))
if (options && options.silent) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
对于 actions 和 mutations 集合,是在 installModule函数 中添加的,稍后会看到。 现在我们来看下commit中的 _withCommit。
_withCommit (fn) {
const committing = this._committing //false
this._committing = true //true
fn()
this._committing = committing //false
}
只有 _withCommit 方法,才能更改当前 committing 的状态,也就是标识当前 state 是 Vuex 内部修改的。
③ 严格模式
在 resetStoreVM 中调用 enableStrictMode 函数会用到 this.strict 做判断来开启严格模式,具体在 resetStoreVM 中会说到。
有一点需要注意的是。严格模式下会观测所有 state 的变化,有一定性能开销,线上建议关闭。
④ installModule
把我们通过 options 传入的各种属性模块注册和安装,如果当前 module 中存在 modules 这个属性会被递归调用。
已实例来说,会被调用三次,分别是 rootModule,module1 和 module2。
// ①构造函数调用 installModule(this, state, [], this._modules.root)
// ②自身递归调用 installModule(store, rootState, path.concat(key), child, hot)
// store 表示当前 Store 实例,rootState 表示根 state,path 表示当前嵌套模块的路径数组,module 表示当前安装的模块,hot 当动态改变 modules 或者热更新的时候为 true。
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
// namespace 在只在一种情况下有值:
// namespaced = true && 当前 module 不是 rootModule
// (未设置 namespaced 为 true 和 rootModule,得到的 namespace 都为空)
// 所以 rootModule 和 module1 都为空
// module2 为 ['module2']
const namespace = store._modules.getNamespace(path)
// 在 _modulesNamespaceMap 注册 module.namespaced 为 true 的 module
// 递归结束后这里存放了 module2
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// 设置 state
// ① rootModule 不进行设置,② 热更新 true 不进行设置
if (!isRoot && !hot) {
// 找到当前 module 的 父module,和 moduleName
// module1 找的就是 rootModule,module2 同理
// 所以 state 自动以 moduleName 来做命名空间了
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
// Vuex 自己修改了 state
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
// 分别对 rootModule,module1,module2 制作对应的上下文对象
const local = module.context = makeLocalContext(store, namespace, path)
module.forEachMutation((mutation, key) => {
// 给每个 module._rawModule 下的每个 mutation 方法添加命名空间
const namespacedType = namespace + key
// 把当前 module 中 mutation 方法都放入当前 Store 实例的 _mutations
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
// 给每个 module._rawModule 下的每个 action 方法添加命名空间
const namespacedType = namespace + key
// 把当前 module 中 action 方法都放入当前 Store 实例的 _actions
registerAction(store, namespacedType, action, local)
})
module.forEachGetter((getter, key) => {
// 给每个 module._rawModule 下的每个 getter 方法添加命名空间
const namespacedType = namespace + key
// 把当前 module 中 getter 方法都放入当前 Store 实例的 _wrappedGetters
registerGetter(store, namespacedType, getter, local)
})
//递归安装当前 module._children 中的 module
module.forEachChild((child, key) => {
// rootModule 安装,传入 module1 安装,然后在传入 module2
// rootModule 的 path 是[],以后每个模块的名字都被 concat入path
installModule(store, rootState, path.concat(key), child, hot)
})
}
这里比较重要的是** namespace,state,makeLocalContext(制作上下文),registerMutation,registerAction,registerGetter**。
(1) namespace:
namespace 在只在一种情况下有值,
当前 module 的 namespaced 传入 true,并且当前 module 不是 rootModule
未设置 namespaced 为 true 和 rootModule (因为 path 为[]),得到的 namespace 都为空。
(2) state:
state 一直是 options 中传入的 state,递归之后变成如下,自动由 moduleName 来做了命名空间。
按照逻辑热更新状态下,安装 module,是不会改变 state,先记下逻辑,后面会深入。
(3) makeLocalContext (制作上下文):
// 针对不同 module 制作局部的 dispatch, cimmit, getters 和 state方法
// 如果没有 namespace, 就用 root 上的方法
// path 暂未用到
function makeLocalContext (store, namespace, path) {
// 未设置 namespaced 为 true 的时候,得到的 namespace 都为空
const noNamespace = namespace === ''
const local = {
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
//如果传入 option ,或者里面有 root 这个属性,type 不做处理,不加命名空间
if (!options || !options.root) {
type = namespace + type
if (!store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
return store.dispatch(type, payload)
},
commit: noNamespace ? store.commit : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
//如果传入 option,或者里面有 root 这个属性,type 不做处理,不加命名空间
if (!options || !options.root) {
type = namespace + type
if (!store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
// 在 local 添加 getters 和 state 两个属性,分别只定义了 get 方法
// 只有被调用的时候才会触发 get 方法
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
//根据 path 来找出 state
get: () => getNestedState(store.state, path)
}
})
return local
}
定义了一个 local 本地化对象,在上面挂载 getters 和 state 属性,dispatch,commit 方法,本质就是根据 noNamespace 做判断,来找到相应的方法和属性。
makeLocalGetters 和 getNestedState 方法都很简单,不细说了。
这里有两点其实还是很模糊:
- local 到底为我们做了什么?
- getters 和 state 里面的 store.getters 和 store.state 是什么时候绑定的。(目前为止还没有,绑定 getters 在 resetStoreVM 里,后面我们会看到。)
(4) registerMutation,registerAction,registerGetter:
...
function installModule() {
...
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)
})
...
}
...
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
// mutation 能访问 state,还能携带参数
// 如果是 rootModule 的mutation 默认访问的就是 rootState
// 如果是 module1 就访问 module1 的 state,module2 也一样
handler(local.state, payload)
})
}
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload, cb) {
// action 能访问下面6个属性
let res = handler({
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
// 不是 promise 就返回 promise 也就是一开始为什么要断言 Promise
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
}
})
}
function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
console.error(`[vuex] duplicate getter key: ${type}`)
return
}
store._wrappedGetters[type] = function wrappedGetter (store) {
// getter 能访问下面4个属性
return rawGetter(
local.state,
local.getters,
store.state,
store.getters
)
}
}
installModule 全部完成之后,我们的 Store 实例发生了变化
不难发现,由于 namespaced 为 false,我们无论是在 rootModule 还是在 module1 里面 dispatch fetchApi,都是会执行两次,fetchApi 里面的 commit 方法,也会触发两次,修改2个 module 的 state。
所以 module.context ( local 对象)就是为了 namespaced 而存在的。
本文开头的问题1,便找到了答案。
action mutation getter 的回调函数里面的是怎么放入一个 Store 实例相关对象的(包含:dispatch,commit,state,rootState,getters,rooteGetters等)。
-
在installModule阶段,首先制作了当前 module 的执行上下文,而上下文对象中包含了当前上下文的需要的属性**(state,getters,rootState,rootGetters)和 dispatch 和 commit 方法**。(其中要注意的是当 module 的 namespaced 为 true 时,dispatch 和 commit 触发的时候自动帮我们加上了 namespace ,这也就是我们能在 module2 中 commit('changeName') 更改的是 module2 中 state 的 name 的)
-
在制作完上下文之后,便开始注册我们的 options 中的 action,mutation 和 getters,分别放入 Store 实例的 _actions,_mutations 和 _wrappedGetters 中,然后通过 local 对象和 Store 实例对象来组成传入工具对象,传入到我们定义的 action,mutation 和 getters 的回调函数中。
⑤ resetStoreVM
带着上面 getters 和 store.state 何时绑定的疑问,我们进入 resetStoreVM。
resetStoreVM方法就是初始化 store._vm,观测 state 和 getters 的变化。
function resetStoreVM (store, state, hot) {
// 初始化是没有设置过这个值的
const oldVm = store._vm
// 绑定 store 实例上的 getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// 全部放入 computed 对象
computed[key] = () => fn(store)
// 定义 store.getters 下每个 getter 的访问器属性
Object.defineProperty(store.getters, key, {
//每次 get 都会从 _vm 去寻找
get: () => store._vm[key],
enumerable: true
})
})
// 用一个 Vue 实例来存储 state 树。
// 有时全局的 mixins 会报错,先让 Vue 忽略。
const silent = Vue.config.silent
Vue.config.silent = true
// 创建storeVM
store._vm = new Vue({
data: {
// state对象终于挂到了一个东西上
$$state: state
},
// 用计算属性来计算 getters,所以可以通过 store._vm.xxx 访问(代理)
computed
})
// 至此 this.state 才能被访问到
Vue.config.silent = silent
为新的 vm 开启严格模式。
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
// 热更新的话就直接清空 oldVm 的 state
store._withCommit(() => {
oldVm._data.$$state = null
})
}
// 销毁老的 storeVM
Vue.nextTick(() => oldVm.$destroy())
}
}
问题3:
state 和 getter 是不是通过 new Vue 来使其变为响应式的。
- 是的,每次在执行 resetStoreVM 的时候都会 new Vue() 来创建一个新的 store ._vm, 然后把老的 vm 销毁,并且我们看到,state 和 getters 都是挂载在新的 store._vm 上的。
get state & set state
class Store{
...
get state () {
// resetStoreVM 后才能获取到
return this._vm._data.$$state
}
set state (v) {
// 修改 state 只能通过 store.replaceState()
assert(false, `Use store.replaceState() to explicit replace store state.`)
}
...
}
问题4:
我们不能直接修改 state 是怎么做到的。
- 当前我们设置 this.$store.state 的时候,会断言报错,并告诉我们只能通过 store.replaceState() 来修改 state,而 this.$store.state 其实并未访问到真实 state ,只是代理 store._vm 上的 state,加上前面我们说的标识 committing,我就可以知道怎么修改 state 成功而不报错:
changeState() {
// _withCommit 修改 committing 标识。
this._withCommit((state) => {
this._vm._data.$$state = state
})
}
而我们看下上面提到的 store.replaceState()
方法:
replaceState (state) {
this._withCommit(() => {
this._vm._data.$$state = state
})
}
和我们预想的一样,所以外部修改 state,就需要调用 store.replaceState() 来假装自己是内部的修改,直接修改的就是一个断言函数,什么都没有。
enableStrictMode
resetStoreVM 中还通过 strict 判断来执行 enableStrictMode 函数,我们来看一下:
function enableStrictMode (store) {
// this 是 store._vm
store._vm.$watch(function () { return this._data.$$state }, () => {
// _committing 在这里派上用场了
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}, { deep: true, sync: true })
}
store._vm.$watch开启了深监听,必然消耗性能的,所以线上建议关闭。
主要流程到这里就过完了,其他中还有些工具类方法和简单的方法,需要了解的再往下看,或者去源码中拜读就行。
读源码必然枯燥,但也是能学习尤大大代码的最短捷径,没有之一,无论是代码组织结构,功能模块解耦,函数职责单一,命名简单而又不简单,设计思路等都是我们非常直接借鉴的地方。
本文还有很多不足,有不对的地方望大家及时指出改正。
其他
动态注册 module 和注销 module。
Store类提供了两个方法:registerModule 和 unregisterModule。
也就是我们的问题2。
registerModule (path, rawModule) {
if (typeof path === 'string') path = [path]
assert(Array.isArray(path), `module path must be a string or an Array.`)
this._modules.register(path, rawModule)
// 把当前 module 安装到当前实例
installModule(this, this.state, path, this._modules.get(path))
// 重设当前 store._vm,其实就是删了重新建一个
resetStoreVM(this, this.state)
}
unregisterModule (path) {
if (typeof path === 'string') path = [path]
assert(Array.isArray(path), `module path must be a string or an Array.`)
// 删除同理
this._modules.unregister(path)
// 这里已经从父级上删除了 state 了
this._withCommit(() => {
const parentState = getNestedState(this.state, path.slice(0, -1))
Vue.delete(parentState, path[path.length - 1])
})
resetStore(this)
}
path 是数组,每次只取数组最后一位来做命名空间,register 方法其实是在指定父 module 上插入 _children :
- 如果 path 是 ['module3'],则会创建一个和 rootModule 平级的 module。
- 如果 path 是 ['module2', 'module3'],则会在 module2 的 _children 添加 module3。
this.state 是在 Store 实例化完成之后才可以访问的属性,所以也不难推断出,这两个方法是为 Store 实例服务的。
我们注意到在 unregisterModule
中,我们首先就从父级上删掉了我们要注销的 state,然后往下走到resetStore方法。
function resetStore (store, hot) {
store._actions = Object.create(null)
store._mutations = Object.create(null)
store._wrappedGetters = Object.create(null)
store._modulesNamespaceMap = Object.create(null)
const state = store.state
// init all modules
installModule(store, state, [], store._modules.root, true)
// reset vm
resetStoreVM(store, state, hot)
}
可以知道
- 动态注销 module ,是要又从 rootModule 开始递归安装 module,很浪费性能。
- installModule 中 hot 传了 true,就不会改变 state,说明下面这段逻辑不会执行。
function installModule (store, rootState, path, module, hot) {
...
// 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)
})
}
...
}
由于 hot 为 true,state 保持不变(之前已经被干掉了,也不需要变)。
subscribe
Store 实例提供了 subscribe 接口,作用是订阅 store 的 mutation。
subscribe (fn) {
const subs = this._subscribers
if (subs.indexOf(fn) < 0) {
subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}
接受的参数是一个回调函数,会把这个回调函数保存到 this._subscribers 上,并返回一个函数,当我们调用这个返回的函数,便可以解除当前函数对 store 的 mutation 变化的监听。
mapState
mapState 工具函数会将 Store 实例中的 state 映射到局部计算属性中。
使用示例:
import { mapState } from 'vuex'
export default {
computed: mapState({
nameGetter1: state => state.name,
nameGetter2: 'name',
nameGetter3 (state) {
return state.name
},
}),
mapState(['name'])
}
mapState 函数可以接受一个对象,也可以接收一个数组,走进函数看一下:
export function mapState (states) {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
return typeof val === 'function'
? val.call(this, this.$store.state, this.$store.getters)
: this.$store.state[val]
}
})
return res
}
函数首先使用了normalizeMap
来格式化传入的参数,进入这个函数看一下:
function normalizeMap (map) {
return Array.isArray(map)
// 传入的参数是数组
// map = [{name: 'name', val: 'name'}]
? map.map(key => ({ key, val: key }))
// 传入的参数是对象
// map = [
// {name: 'nameGetter1', val: state => state.name},
// {name: 'nameGetter2', val: 'name'},
// {name: 'nameGetter3', val(state) {return state.name }},]
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
-
回到 mapState 函数,在调用了 normalizeMap 函数后,把传入的 states 转换成由 {key, val} 对象构成的数组,接着调用 forEach 方法遍历这个数组,构造一个新的对象,这个新对象每个元素都返回一个新的函数 mappedState,函数对 val 的类型判断,如果 val 是一个函数,则直接调用这个 val 函数,把当前 store 上的 state 和 getters 作为参数,返回值作为 mappedState 的返回值;否则直接把 this.$store.state[val] 作为 mappedState 的返回值。
-
那么为何 mapState 函数的返回值是这样一个对象呢,因为 mapState 的作用是把全局的 state 和 getters 映射到当前组件的 computed 计算属性中,我们知道在 Vue 中 每个计算属性都是一个函数。 为了更加直观地说明,回到刚才的例子:
import { mapState } from 'vuex'
export default {
computed: mapState({
nameGetter1: state => state.name,
nameGetter2: 'name',
nameGetter3 (state) {
return state.name
},
}),
mapState(['name'])
}
经过 mapState 函数调用后的结果,如下所示:
import { mapState } from 'vuex'
export default {
// ...
computed: {
nameGetter1() {
return this.$store.state.name
},
nameGetter2() {
return this.$store.state['name']
},
nameGetter3() {
return this.$store.state.name
},
name() {
return this.$store.state.name
}
}
}
mapActions
,mapGetters
,mapMutations
实现方式都差不多,这里就不多赘述,如果需要了解就去源码中查看。