在浏览 Vuex 源码之前,因为 Vuex 的 api 和应用性能稍微简单,默认认为实现起来相当简单,望而却步。然而通过深刻学习源码,发现外围性能联合 vue 实现起来十分奇妙,也就外围几行代码,直呼外行。本文也就 100 左右行代码就能疾速手写本人的 Vuex 代码!
前言
Vuex 是⼀个专为 Vue.js 应⽤程序开发的状态管理模式。它采⽤集中式存储管理应⽤的所有组件的状态,并以相应的规定保障状态以⼀种可预测的⽅式发⽣变动。那么 Vuex 和单纯的全局对象有什么不同呢?
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发⽣变 化,那么相应的组件也会相应地失去⾼效更新。
不能间接扭转 store 中的状态。扭转 store 中的状态的唯⼀路径就是显式地提交 (commit) mutation。这样使得咱们能够⽅便地跟踪每⼀个状态的变动,从⽽让咱们可能实现⼀些⼯具帮忙我 们更好地理解咱们的应⽤。
通过以上两点认知咱们来疾速实现本人的 Vuex!
Vuex 初始化
为什么在 vue 实例化的时候要传入 store
去实例化呢?那是为了让 vue
所有的组件中能够通过 this.$store
来获取该对象,即 this.$store
指向 store
实例。
// Store 待实现
const store = new Store({
state: {
count: 0,
num: 10
})
new Vue({
el: '#app',
store: store // 此处的 store 为 this.$options.store
})
Vuex
提供了 install
属性,通过 Vue.use(Vuex)
来注册。
const install = function (Vue) {
Vue.mixin({beforeCreate() {if (this.$options.store) {Vue.prototype.$store = this.$options.store}
}
})
}
Vue 全局混⼊了⼀个 beforeCreated
钩⼦函数,options.store
保留在所有组件的 this.$store
中,这个 options.store
就是咱们在实例化 Store
对象的实例。Store 对象的构造函数接管⼀个对象参数,它蕴含 actions
、getters
、state
、mutations
等核⼼概念,接下来咱们一一实现。
Vuex state
其实 state 是 vue 实例中的 data,通过 Store 外部创立 Vue 实例,将 state 存储到 data 里,而后扭转 state 就是触发了 data 数据的扭转从而实现了视图的更新。
// 实例化 Store
const store = new Store({
state: {
count: 0,
num: 10
}
})
// Store 实现
class Store {constructor({state = {}}) {
this.vm = new Vue({data: {state} // state 增加到 data 中
})
}
get state() {return this.vm.state // 将 state 代理到 vue 实例中的 state}
set state(v) {console.warn(`Use store.replaceState() to explicit replace store state.`)
}
}
由上可知,store.state.count
等价于 store.vm.state
。不论是获取或者扭转 state 外面的数据都是间接的触发了 vue 中 data 数据的变动,从而触发视图更新。
Vuex getters
晓得 state
是vue
实例中的data
,那么同理,getters 就是 vue 中的计算属性 computed。
// 实例化 Store
const store = new Store({
state: {
count: 0,
num: 10
},
getters: {
total: state => {return state.num + state.count}
},
})
// Store 实现
class Store {constructor({state = {}, getters = {}}) {
this.getters = getters
// 创立模仿 computed 对象
const computed = {}
Object.keys(getters).forEach(key => {const fn = getters[key]
// 入参 state 和 getters
computed[key] = () => fn(this.state, this.getters)
// 代理 getters 到 vm 实例上
Object.defineProperty(this.getters, key, {get: () => this.vm[key]
})
})
// 赋值到 vue 中的 computed 计算属性中
this.vm = new Vue({
data: {state,},
computed,
})
}
get state() {return this.vm.state}
set state(v) {console.warn(`Use store.replaceState() to explicit replace store state.`)
}
}
应用 Object.defineProperty
将 getters 上的所有属性都代理到了 vm 实例上的 computed 计算属性中,也就是 store.getters.count
等价于 store.vm.count
。
Vuex mutations
mutations 等同于公布订阅模式,先在 mutations 中订阅事件,而后再 commit 公布事件。
// 实例化 Store
const store = new Store({
state: {
count: 0,
num: 10
},
mutations: {INCREASE: (state, n) =>{state.count += n},
DECREASE: (state, n) =>{state.count -= n}
}
})
// Store 实现
class Store {constructor({state = {}, mutations = {}, strict = false}) {
this.mutations = mutations
// 严格摸索只能通过 commit 扭转 state
this.strict && this.enableStrictMode()}
commit(key, payload) {
// 获取事件
const fn = this.mutations[key]
// 开始 commit
this.committing = true
// 执行事件 并传参
fn(this.state, payload)
// 完结 commit 所以阐明 commit 只能执行同步事件
this.committing = false
}
enableStrictMode () {
// vm 实例察看 state 是否由 commit 触发扭转
this.vm.$watch('state', () => {
!this.committing
&&
console.warn(`Do not mutate vuex store state outside mutation handlers.`)
}, {deep: true, sync: true})
}
get state() {return this.vm.state}
set state(v) {console.warn(`Use store.replaceState() to explicit replace store state.`)
}
}
store.commit
执行 mutations
中的事件,通过公布订阅实现起来并不难。Vuex
中的严格模式,只能在 commit
的时候扭转 state
数据,不然提醒谬误。
Vuex actions
mutations
用于同步更新 state
,而 actions
则是提交 mutations
,并可进行异步操作,从而间接更新 state
。
// 实例化 Store
const store = new Store({
actions: {getToal({dispatch, commit, state, getters}, n){
return new Promise(resolve => {setTimeout(() => {commit('DECREASE', n)
resolve(getters.total)
}, 1000)
})
}
}
})
// Store 实现
class Store {constructor({actions = {}}) {this.actions = actions}
dispatch(key, payload) {const fn = this.actions[key]
const {state, getters, commit, dispatch} = this
// 留神 this 指向
const result = fn({state, getters, commit: commit.bind(this), dispatch: dispatch.bind(this)}, payload)
// 返回 promise
return this.isPromise(result) ? result : Promise.resolve(result)
}
// 判断是否是 promise
isPromise (val) {return val && typeof val.then === 'function'}
}
mutations
和 actions
的实现大同小异,actions
外围在于解决异步逻辑,并返回一个 promise
。
残缺案例代码
这边把以上的代码对立归纳起来,能够依据这份残缺代码来剖析 Vuex
逻辑。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
</head>
<body>
<div id="app">
<child-a></child-a>
<child-b></child-b>
</div>
<script>
class Store {constructor({state = {}, getters = {}, mutations = {}, actions = {}, strict = false}) {
this.strict = strict
this.getters = getters
this.mutations = mutations
this.actions = actions
this.committing = false
this.init(state, getters)
}
get state() {return this.vm.state}
set state(v) {console.warn(`Use store.replaceState() to explicit replace store state.`)
}
init (state, getters) {const computed = {}
Object.keys(getters).forEach(key => {const fn = getters[key]
computed[key] = () => fn(this.state, this.getters)
Object.defineProperty(this.getters, key, {get: () => this.vm[key]
})
})
this.vm = new Vue({data: {state},
computed,
})
this.strict && this.enableStrictMode()}
commit(key, payload) {const fn = this.mutations[key]
this.committing = true
fn(this.state, payload)
this.committing = false
}
dispatch(key, payload) {const fn = this.actions[key]
const {state, getters, commit, dispatch} = this
const res = fn({state, getters, commit: commit.bind(this), dispatch: dispatch.bind(this)}, payload)
return this.isPromise(res) ? res : Promise.resolve(res)
}
isPromise (val) {return val && typeof val.then === 'function'}
enableStrictMode () {this.vm.$watch('state', () => {!this.committing && console.warn(`Do not mutate vuex store state outside mutation handlers.`)
}, {deep: true, sync: true})
}
}
const install = function () {
Vue.mixin({beforeCreate() {if (this.$options.store) {Vue.prototype.$store = this.$options.store}
}
})
}
// 子组件 a
const childA = {
template: '<div><button @click="handleClick">click me</button> <button @click="handleIncrease">increase num</button> <button @click="handleDecrease">decrease num</button></div>',
methods: {handleClick() {this.$store.state.count += 1},
handleIncrease() {this.$store.commit('INCREASE', 5)
},
handleDecrease() {this.$store.dispatch('getToal', 5).then(data => {console.log('total', data)
})
}
}
}
// 子组件 b
const childB = {template: '<div><h1>count: {{ count}}</h1><h1>total: {{total}}</h1></div>',
mounted() {
// 严格模式下批改 state 的值将正告
// this.$store.state.count = 1
},
computed: {count() {return this.$store.state.count},
total(){return this.$store.getters.total}
}
}
const store = new Store({
state: {
count: 0,
num: 10
},
getters: {
total: state => {return state.num + state.count}
},
mutations: {INCREASE: (state, n) =>{state.count += n},
DECREASE: (state, n) =>{state.count -= n}
},
actions: {getToal({dispatch, commit, state, getters}, n){
return new Promise(resolve => {setTimeout(() => {commit('DECREASE', n)
resolve(getters.total)
}, 1000)
})
}
}
})
Vue.use({install})
new Vue({
el: '#app',
components: {
'child-a': childA,
'child-b': childB
},
store: store
})
</script>
</body>
</html>
总结
通过下面的残缺案例可知,Vuex
外围代码也就 100
行左右,然而他奇妙的联合了 vue
的data
和 computeds
属性,化繁为简,实现了简单的性能,所以说 vuex
是不能脱离 vue
而独立运行的。
本文是联合官网源码提取核心思想手写本人的 Vuex
,而官网的Vuex
,为了防止store
构造臃肿,还实现了 modules
等性能,具体实现能够查看 Vuex 官网源码。