前言
相信用过Vue的同学都知道Vuex状态管理,关于Vuex的基本使用可以在各大论坛找到相关资料,但是对于使用相对不太频繁的同学来说需要通过不断百度或者对照文档才能进行开发,基于存在这种问题,本文通过一些小demo对Vuex核心概念做一个大概的梳理(不包含辅助方法的实现),介绍涉及到的实现源码以及设计思想。要求了解Vuex的一些基本概念以及基本用法。
主要内容
Vuex核心思想及基本使用(预热)
Vuex核心思想
我们知道Vuex 应用的核心就是 store,简称仓库。可以理解为是一个原生JavaScript对象,包含特殊的一些属性和方法,对比定义一个单纯全局对象,Vuex官方总结主要区别体现为下面两点。
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
对于上述文字,有人可能会先抛出几个疑问:
- 对于Vuex中是如何实现响应式?
- 为什么要通过显示提交mutation来更改state状态,而不是直接修改?
- 跟踪每一个状态的变化具体体现在哪里?
围绕这几个核心问题,后面会讲到原因,请耐心往下看。
基本使用
起一个简单的Vuex项目,基本的项目结构主要包含一下几个文件:
src/App.vue
src/main.js
src/components/ProductListOne.vue
src/components/ProductListTwo.vue
其他不涉及的就不一一罗列了。
App.vue
引用两个组件,分别是ProductListOne和ProductListTwo。
<template>
<div id="app">
<product-list-one v-bind:products="products"></product-list-one>
<product-list-two v-bind:products="products"></product-list-two>
</div>
</template>
<script>
import ProductListOne from './components/ProductListOne.vue'
import ProductListTwo from './components/ProductListTwo.vue'
export default {
name: 'app',
components: {
'product-list-one': ProductListOne,
'product-list-two': ProductListTwo
},
data () {
return {
products: [
{name: '可乐', price: 20},
{name: '鸡翅', price: 40},
{name: '游戏', price: 60},
{name: '显示屏', price: 80}
]
}
}
}
</script>
<style>
body{
font-family: Ubuntu;
color: #555;
}
</style>
main.js
初始化Vuex的基本结构,定义几个简单的操作state的getters、mutations和actions方法。
import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex'
Vue.use(Vuex);
const store = new Vuex.Store({
//待添加
state: {
products: [
{name: '可乐', price: 20},
{name: '鸡翅', price: 40},
{name: '游戏', price: 60},
{name: '显示屏', price: 80}
]
},
//相当于computed
getters:{ //添加getters
saleProducts: (state, getters) => {
console.log(state, getters)
let saleProducts = state.products.map( product => {
return {
name: product.name,
price: product.price / 2
}
})
return saleProducts;
}
},
mutations:{ //添加mutations
minusPrice (state, payload ) {
let newPrice = state.products.forEach( product => {
product.price -= payload
})
}
},
actions:{ //添加actions
minusPriceAsync( context, payload ) {
setTimeout( () => {
context.commit( 'minusPrice', payload ); //context提交
}, 2000)
}
}
})
new Vue({
el: '#app',
store,
render: h => h(App)
})
列表一ProductListOne.vue
<template>
<div id="product-list-one">
<h2>Product List One</h2>
<ul>
<li v-for="(product,index) in products" :key="index">
<span class="name">{{ product.name }}</span>
<span class="price">${{ product.price }}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
// props: ['products'],
data () {
return {
products: this.$store.getters.saleProducts
}
}
}
</script>
<style scoped>
#product-list-one{
background: pink;
box-shadow: 1px 2px 3px rgba(0,0,0,0.2);
margin-bottom: 30px;
padding: 10px 20px;
}
#product-list-one ul{
padding: 0;
}
#product-list-one li{
display: inline-block;
margin-right: 10px;
margin-top: 10px;
padding: 20px;
background: rgba(255,255,255,0.7);
}
.price{
font-weight: bold;
color: #E8800C;
}
</style>
列表二ProductListTwo.vue
<template>
<div id="product-list-two">
<h2>Product List Two</h2>
<ul>
<li v-for="(product, index) in products" :key="index">
<span class="name">{{ product.name }}</span>
<span class="price">${{ product.price }}</span>
</li>
<button @click="minusPrice">减少价格</button>
<button @click="minusPriceAsync">异步减少价格</button>
</ul>
</div>
</template>
<script>
export default {
// props: ['products'],
data () {
return {
products: this.$store.state.products
}
},
methods: {
minusPrice(){
this.$store.commit("minusPrice", 2);
},
minusPriceAsync() {
this.$store.dispatch('minusPriceAsync', 5); //分发actions中的minusPriceAsync这个异步函数
}
}
}
</script>
<style scoped>
#product-list-two{
background: #ccc;
box-shadow: 1px 2px 3px rgba(0,0,0,0.2);
margin-bottom: 30px;
padding: 10px 20px;
}
#product-list-two ul{
padding: 0;
list-style-type: none;
}
#product-list-two li{
margin-right: 10px;
margin-top: 10px;
padding: 20px;
background: rgba(255,255,255,0.7);
}
.price{
font-weight: bold;
color: #860CE8;
display: block;
}
</style>
以上是基本的Vuex使用代码,下面在讲述一些原理的时候可以对照上面的案例帮助理解。
Vuex基本原理解析
下面将从Vuex的安装到实现数据存取的过程进行讲解。建议下载一套Vuex源码进行对比学习。
安装过程
在Vuex初始化的过程中,首先会经过安装操作,通过vue.use方法进行install安装,安装之前会对当前环境进行判断是否已经安装过Vuex,以避免重复安装。
//store.js中安装方法
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.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
applyMixin(Vue)
}
最后调用applyMixin方法将vuexInit(Vuex初始化的钩子函数)方法(存在mixin.js中)混入Vue组件的beforeCreate钩子中。其中vuexInit方法中对传入的store进行处理,以保证所有组件都共用同一份store。那么此时我们会想到,Vuex就是为了解决组件之间的数据共享问题,通过这种方式,可以实现每个组件访问的都是同一个数据仓库(store),接着往下看。
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) {
/*子组件直接从父组件中获取$store,这样就保证了所有组件都公用了全局的同一份store*/
this.$store = options.parent.$store
}
}
接下来在源码store.js中的constructor中,大部分逻辑是对定义一些下面需要用到的数组对象、是否为严格模式、以及定义用来实现watch的vue实例等,我们可以暂时不用管。主要是看下面两个函数:
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
/*初始化根module,这也同时递归注册了所有子modle,收集所有module的getter到_wrappedGetters中去,this._modules.root代表根module才独有保存的Module对象*/
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
resetStoreVM(this, state)
第一个方法是实现对模块进行安装,主要通过命名空间标识的方式以及创建局部上下文的方式实现对子模块的管理和响应。
第二个方法主要是实现注册state及绑定getter。通过Object.defineProperty实现每一个getter的get操作,比如获取this.$store.getters.test的时候获取的是store._vm.test方法,返回定义后的方法给computed属性,为实现Vuex的响应式做准备。
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
})
})
接下来这段代码,通过new一个Vue对象,借助Vue内部的响应式原理实现注册state以及computed,来实现Vuex的响应式操作。好了,那么文中一开始提到的第一个问题已经回答了!
store._vm = new Vue({
data: {
$$state: state
},
computed
})
第二问题:为什么更改state状态只能通过提交mutation的方式才能实现修改?
非严格模式下直接使用this.store.state.xxx修改state不会发生改变, 并且控制台不会发生报错。而使用严格模式下面会直接进行报错,原因在于使用严格模式的时候会检测store._committing(在Store的constructor中有定义)状态,当store._committing的值为false,表示不是通过mutation的方法修改的,你可以通过控制台直接操作store中的state数据,手动修改它,并打印此时store._committing的值。当为严格模式的时候,执行下面代码:
// enable strict mode for new vm
/* 使用严格模式,保证修改store只能通过mutation */
if (store.strict) {
enableStrictMode(store)
}
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
if (process.env.NODE_ENV !== 'production') {
/* 检测store中的_committing的值,如果是false代表不是通过mutation的方法修改的 */
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
所以说Vuex更改state状态是必须要通过显示提交mutation的方式,当调用commit()提交一个mutation的时候,store._committing的值就变为true。
说说vuex数据存取
看回最上面的demo例子,我在main.js中new了一个vuex的store实例,通过打印store实例我们可以获取到一些方法和属性。
外部获取state属性
当通过store.state访问根state的时候,实际上是访问的是下面的state方法,该方法会返回store._vm._data.$$state对象
get state () {
return this._vm._data.$$state
}
其实是返回resetStoreVM方法中通过Vuex借助vue的响应方式注册state属性,并且达到双向绑定的效果。
store._vm = new Vue({
data: {
$$state: state
},
computed
})
外部获取getter属性
同理,resetStoreVM中通过Object.defineProperty为每一个getter方法设置get方法,在new Vue的时候会注册computed属性,其作用就是为了实现getter方法的响应式效果,我们可以通过store.getters来获取定义的getter的返回值。
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
})
})
内部获取state和getters的实现
我们可以在demo实例中的main.js中的saleProducts方法中看到参数state和getter,为什么可以在getter内部获取到当前module下的state对象以及getters,原因在于在注册getter的时候,即在registerGetter方法中,传入当前通过makeLocalContext方法获取的local对象,传递给每个getter。实现在local对象中可以获取到独立的state和getters。
/* 包装getter */
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
关键在于local是怎么获取的?我们来看看makeLocalContext中的实现逻辑。
//通过判断是否存在命名空间来判断返回对应的getter
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
//获取当期模块的getters jmin
function makeLocalGetters (store, namespace) {
const gettersProxy = {}
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
//type对应所有getters中的某一个key
// skip if the target getter is not match this namespace
if (type.slice(0, splitPos) !== namespace) return
// extract local getter type
const localType = type.slice(splitPos)
// Add a port to the getters proxy.
// Define as getter property because
// we do not want to evaluate the getters in this time.
//store.getters[type]则对应的是子module中getters的getter参数
Object.defineProperty(gettersProxy, localType, {
get: () => store.getters[type],
enumerable: true
})
})
return gettersProxy
}
这边Object.defineProperties定义的两个属性,根据是否存在命名空间来返回对应的getters或者state,子模块中会进入makeLocalGetters/getnestedState方法中对根据当前命名空间对store中所有的getters进行过滤操作,得到对应的local type ,返回gettersProxy给外层getters。
执行commit、dispatch操作之后做了什么
在了解这个原理之前首先要明白这两个方法的使用,我们通过commit方法来提交同步的mutation方法来修改state数据;当然,也可以通过dispatch的方法分发一个actions,实现异步commit我们的mutations。例如在上面的demo中main.js中的actions方法,通过一个promise来异步提交minusPrice这个方法,实现对state的异步修改。
当我们执行commit方法时,通过提交mutation的方式来修改state数据,该方法主要逻辑是同步执行一个mutation方法,其实就是执行一下我们定义的同步方法而已。首先通过传进的_type(我们定义的mutation函数名)取出在mutations集合中的对应的方法。如果存在,那么执行定义在mutations中的所有同名方法。
这里有个注意的地方是,如果我们在多个module定义了同名的mutations方法,都会被一一执行。
/* 调用mutation的commit方法 */
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]
//如果不存在对应的mutation或者拼写错误,则会进入这个方法
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
/* 执行mutation中的所有方法 */
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
//执行我们定义的mutation函数
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一个actions来实现异步commit一个mutation,方法中同样是取出type对应的action,不同的是dispatch会返回一个promise对象。因为我们提交actions是支持异步的,自然的,我们在注册actions的时候也需要对函数进行promise化。接下来说一下actions注册过程。
/* 调用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)
}
在注册actions的时候,在处理参数过程汇总供了多个参数供外部使用,如支持dispatch多个异步方法,或者commit多个mutation等。res函数为自定义handler执行后的返回结构,其参数可以根据自身需求进行自定义,结果都会返回一个新的promise,以便在上面dispatch的时候进行处理。
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)
}
插件原理
Logger插件实现原理
通过createLogger方法创建一个日志插件,用来辅助代码逻辑的调试,使得我们可以从外部追踪store内部的变化。
export default function createLogger ({
collapsed = true,
filter = (mutation, stateBefore, stateAfter) => true,
transformer = state => state,
mutationTransformer = mut => mut
} = {}) {
return store => {
let prevState = deepCopy(store.state)
store.subscribe((mutation, state) => {
if (typeof console === 'undefined') {
return
}
const nextState = deepCopy(state)
if (filter(mutation, prevState, nextState)) {
const time = new Date()
const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`
const formattedMutation = mutationTransformer(mutation)
const message = `mutation ${mutation.type}${formattedTime}`
const startMessage = collapsed
? console.groupCollapsed
: console.group
// render
try {
startMessage.call(console, message)
} catch (e) {
console.log(message)
}
console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState))
console.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
console.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState))
try {
console.groupEnd()
} catch (e) {
console.log('—— log end ——')
}
}
prevState = nextState
})
}
}
其中的store.subscribe方法,它相当于订阅了 mutation 的提交,它的 prevState 表示之前的 state,nextState 表示提交 mutation 后的 state,这两个 state 都需要执行 deepCopy 方法拷贝一份对象的副本,这样对他们的修改就不会影响原始 store.state。接下来就是打印一些时间相关的消息,更新 prevState = nextState,为下一次提交 mutation 输出日志做准备。
这里再次提到了mutation,这也是为什么修改state数据必须通过提交mutation的方式来操作的一个主要原因。
另外,Logger插件还是调用了浏览器console.log函数实现打印,另外对颜色已经字体大小做了修改。当然我们也可以自己去实现 Vuex 的插件,来帮助我们实现一些特定的需求。
总结
以上主要围绕Vuex的核心思想里面抛出的几个问题进行解析,了解以上基本的实现原理,大概就知道Vuex是怎么实现数据的共享操作,以及更好的在开发环境中利用插件对数据变化进行可视化追踪,以提高调试效率。除此之外Vuex还提供了动态创建子模块、自定义Vuex插件等功能,可以根据自身需求进行扩展使用。