目錄
- install 方法
- Store 類
- State
- Mutations
- Actions
- 小結(jié)一下
- Modules
閑話不多說,讓我們開始實現(xiàn)一個簡單的 Vuex
首先,回想一下 Vuex 的使用方法,這里給出一個簡單的使用例子:
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
a: 1
},
getters: {
aPlus(state){
return state.a + 1
}
},
mutations: {
addA(state, payload){
state.a += payload
}
},
actions: {
asyncAddA({commit, state}, payload){
setTimeout(() => {
commit('addA', payload)
}, 1000)
}
},
modules: {
a: {
modules: {
c: {
state: {
c1: 'c1'
}
}
}
},
b: {
state: {
b: 1
},
mutations: {
bPlus(state, payload){}
},
getters: {
bPlus(state){}
}
}
}
})
// 根組件實例
new Vue({
store
})
可以看到,'vuex' 導出來的應(yīng)該是一個對象,上面有至少兩個屬性:install 和 Store,install 方法在 Vue.use(Vuex) 時會執(zhí)行,Store 是一個類,我們先把大體的架子寫出來:
let Vue // 后面要用到
class Store{
constructor(options){
// ...
}
}
function install(){}
export {
install,
Store
}
1. install 方法
接下來,我們看一下 install 方法的實現(xiàn):
用過 Vuex 的都知道:當在根組件注入 store 后,每個子組件都能訪問到這個 store,這其實就是 install 幫我們做到的,先來看看這個方法的實現(xiàn):
let Vue
function install(_Vue){
// 重復安裝 Vuex
if(Vue) throw new Error('Vuex instance can only exist one!')
Vue = _Vue
// 將 vm.$options.$store 注入到每個子組件的 vm.$store 中
Vue.mixin({
beforeCreate() {
if(this.$options && this.$options.store){
this.$store = this.$options.store
} else {
this.$store = this.$parent && this.$parent.$store
}
}
})
}
Vue.use(Vuex) 會將 Vue 傳入 install 方法,這個全局變量可以用來判斷是否已經(jīng)被 use 過了,重復則報錯。
如果沒有重復,使用 Vue.mixin 混入一段代碼,這段代碼會在 beforeCreate 這個鉤子函數(shù)執(zhí)行之前執(zhí)行
核心代碼是這幾行:
if(this.$options && this.$options.store){
this.$store = this.$options.store
} else {
this.$store = this.$parent && this.$parent.$store
}
this 指向當前組件實例,如果當前組件實例的 $options 上有 store 屬性,說明該實例是注入 store 的根實例,直接往 $store 上掛載 store
反之,else 中會去檢查當前實例的父組件: $parent 上有沒有 $store,有則在實例上掛載該屬性
因為組件的生命周期順序是:父組件先創(chuàng)建,然后子組件再創(chuàng)建,也就是說子組件執(zhí)行 beforeCreate 鉤子函數(shù)時,父組件的 store 已經(jīng)注入了,所以可以實現(xiàn)循環(huán)注入。
2. Store 類
接下來,我們實現(xiàn)一下 Store 類:
Store 中有四部分需要我們實現(xiàn):State、Getter、Mutation、Action,我們先來實現(xiàn) State:
(1) State
通過 $store.state.xxx 即可訪問到 vuex 中的 state 狀態(tài)
class Store{
constructor(options){
this._state = new Vue({ data:options.state })
}
get state(){
return this._state
}
}
這里我們不直接通過 store.state 提供訪問,而是通過訪問器形式提供訪問,可以避免 state 被外部直接修改。
另外要注意的一個點是:我們借用 Vue 讓 state 變成響應(yīng)式數(shù)據(jù)了。這樣的話,當 state 變化的時候,依賴的地方都能得到更新通知
以上,我們簡單實現(xiàn)了 State 狀態(tài)
(2) Getters
接下來實現(xiàn) Getters,Getters 是通過 store.getters.x 的形式訪問的
首先,將 options 中的 getters 遍歷,將每個屬性逐個掛載到 store.getters 上。
因為 getters 的特點是:訪問屬性,返回函數(shù)執(zhí)行,很容易想到可以用訪問器實現(xiàn)。
constructor(options){
// ...
this.getters = {}
Object.keys(getters).forEach(k => {
Object.defineProperty(this._getters, k, {
get: ()=>{ // 箭頭函數(shù)保證 this 能夠訪問到當前 store 實例
return getters[k](this.state)
}
})
})
}
(3) Mutations
mutations 通過 store.commit(type, payload) 觸發(fā),commit 內(nèi)部會通過 type 取到 this._mutations 上對應(yīng)的 mutation,將 payload 傳入并執(zhí)行
我們需要將 options 上的 mutations 上進行遍歷,定義到 this._mutations 上,之所以這樣重新定義一遍是為了能夠在 mutation 函數(shù)外面封裝一層,方便傳入 state
constructor(options){
// ...
this._mutations = {}
Object.keys(mutations).forEach(k => {
this._mutations[k] = (payload) => {
mutations[k](this.state, payload) // 注意 state 參數(shù)的傳入
}
})
}
commit = (type, payload) => { // 箭頭函數(shù),保證 this 指向 store
return this._mutations[type](payload)
}
(4) Actions
actions 的實現(xiàn)和 mutations 相似,就不再贅述了。
不同點在于:
(1) 執(zhí)行回調(diào)傳入的參數(shù)不同
(2) dispatch 返回的應(yīng)該是一個 Promise
constructor(options){
// ...
this._actions = {}
Object.keys(actions).forEach(k => {
this._actions[k] = (payload) => {
actions[k](this, payload) // 這里直接將整個 store 傳入了
}
})
}
dispatch = (type, payload) => {
return new Promise((resolve, reject) => {
try{
resolve(this._actions[type](payload))
} catch (e){
reject(e)
}
})
}
(5) 小結(jié)一下
OK,現(xiàn)在讓我們把上面的這些代碼片段寫到一塊
let Vue
class Store{
constructor(options){
this._state = new Vue({
data: { state: options.state }
})
// 生成 Getters、Mutations、Actions
this.generateGetters(options.getters)
this.generateMutations(options.mutations)
this.generateActions(options.actions)
}
generateGetters(getters = {}){
this.getters = {}
Object.keys(getters).forEach(k => {
Object.defineProperty(this.getters, k, {
get: ()=>{
return getters[k](this.state)
}
})
})
}
generateMutations(mutations = {}){
this._mutations = {}
Object.keys(mutations).forEach(k => {
this._mutations[k] = (payload) => {
mutations[k](this.state, payload)
}
})
}
generateActions(actions = {}){
this._actions = {}
Object.keys(actions).forEach(k => {
this._actions[k] = (payload) => {
actions[k](this, payload)
}
})
}
commit = (type, payload) => {
return this._mutations[type](payload)
}
dispatch = (type, payload) => {
return new Promise((resolve, reject) => {
try{
resolve(this._actions[type](payload))
} catch (e){
reject(e)
}
})
}
get state(){
return this._state.state
}
}
function install(_Vue){
if(Vue) throw new Error('Vuex instance can only exist one!')
Vue = _Vue
Vue.mixin({
beforeCreate() {
if(this.$options && this.$options.store){
this.$store = this.$options.store
} else {
this.$store = this.$parent && this.$parent.$store
}
}
})
}
export default {
install,
Store
}
(6) Modules
接下來,我們要實現(xiàn) Modules。準備好,前面的代碼要發(fā)生變動了
我們先貼一下前面的使用例子:
new Vuex.Store({
// ...
modules: {
a: {
modules: {
c: {
state: {
c1: 'c1'
}
}
}
},
b: {
state: {
b: 1
},
mutations: {
bPlus(state, payload){}
},
getters: {
bPlus(state){}
}
}
}
})
先總結(jié)一下各個屬性的訪問方式:
對于 c 模塊中的狀態(tài) c1,訪問方式為:store.state.a.c.c1
對于模塊的 getters、mutations、actions,都被定義到 store 上了,通過形如 store.getters.xxx 這種方式訪問
好了,現(xiàn)在目標明確了,接下來就把上面提到的兩個點實現(xiàn):
首先,我們需要將 store 上 getters、mutations、actions 進行初始化成空對象
我們不再需要用 generateGetters 等方法對 getters、mutations、actions 這些屬性進行處理了,這部分代碼可以刪掉了。在 installModule 的過程中我們會對這些屬性進行統(tǒng)一處理。
不過不用擔心,我們前面講到的各個屬性的處理方式的核心代碼,后面依舊用得上!
constructor(options){
this._state = new Vue({
data: { state: options.state }
})
// init
this.getters = {}
this.mutations = {}
this.actions = {}
// this.generateGetters(options.getters)
// this.generateMutations(options.mutations)
// this.generateActions(options.actions)
let modules = options // 這里取到的 options,實際上就是根模塊
installModule(this, this.state, [], modules) // 對各個子模塊進行處理的函數(shù),重點關(guān)注!
}
重點關(guān)注一下 installModule 方法,在這個方法里我們實現(xiàn)了 state、getters 等屬性的收集處理
先解釋一下傳給這個函數(shù)的四個參數(shù):
-
store實例 -
store上的state -
path:在installModule中需要遞歸來安裝處理子模塊,所以用path數(shù)組來表示模塊的層級關(guān)系。數(shù)組的最后一位為當前要處理的模塊的模塊名,最后一位前面的都是當前模塊的祖先模塊。舉個栗子:如果是根模塊,傳入的path為[],如果傳入[a, c],說明當前處理模塊名為c,模塊層級為:根模塊 > a模塊 > c模塊 -
module是當前要處理的模塊
我們先來把這個函數(shù)的基本架子寫好,因為模塊的嵌套層數(shù)是未知的,所以必須用遞歸進行模塊處理安裝。
function installModule(store, state, path, module) {
// 如果當前模塊還有 modules(即還有子模塊),遞歸進行模塊安裝
if(module.modules){
// 遍歷當前模塊的子模塊
eachObj(module.modules, (modName, mod)=>{
// 將傳入的 path 拼接當前要處理的的模塊名,得到模塊層級數(shù)組并傳入,進行子模塊安裝
installModule(store, state, path.concat(modName), mod)
})
}
}
注意這里的 path.concat(modName) 是為了將祖先模塊名拼接到 path 中,這個模塊層級數(shù)組在后面需要用到,我們后面會講
另外,這里把遍歷對象的方法封裝到了 eachObj 中,讓代碼看起來簡潔一點:
function eachObj(obj, callback){
Object.keys(obj).forEach(k => {
callback(k, obj[k])
})
}
接下來我們把模塊的 getters、mutations、actions 掛載到根模塊的相應(yīng)屬性上,這三者的處理方式大同小異,要注意的點就是:
同名 mutations、actions 不會被覆蓋,他們會被依次執(zhí)行。所以 store.mutations.xxx 和 store.actions.xxx 應(yīng)該是一個數(shù)組,但 getters 不允許同名,直接掛載到 store.getters 上即可
function installModule(store, state, path, module) {
let getters = module.getters || {}
// 將模塊的 getters 定義到 store.getters 上
eachObj(getters, (k, fn) => {
Object.defineProperty(store.getters, k, {
get(){
return fn(module.state) // 注意這里傳入的 state 是當前模塊的局部 state
}
})
})
let mutations = module.mutations || {}
eachObj(mutations, (k, fn) => {
const rootMut = store.mutations // 根模塊的 mutations
// 先檢查 rootMut[k] 是否被初始化了,沒有的話初始化為空數(shù)組
if(!rootMut[k]) {
rootMut[k] = []
}
rootMut[k].push((payload)=>fn(module.state, payload))
})
// actions 類似 mutations 的實現(xiàn)
let actions = module.actions || {}
eachObj(actions, (k, fn) => {
const rootAct = store.actions
if(!rootAct[k]){
rootAct[k] = []
}
rootAct[k].push((payload)=>fn(store, payload))
})
if(module.modules){ // 遞歸處理模塊
eachObj(module.modules, (modName, mod)=>{
installModule(store, state, path.concat(modName), mod)
})
}
}
接下來我們要實現(xiàn) state 的掛載,這部分代碼相對上面難理解一點:
function installModule(store, state, path, module) {
let parent =
path.slice(0, -1).reduce((state, cur) => {
return state[cur]
}, state)
Vue.set(parent, path[path.length - 1], module.state)
// 省略處理 getters、mutations、actions 的代碼
// 省略遞歸處理模塊的代碼
}
為了保證后面的思路不會亂掉,這里還是要再強調(diào)一下 path 的含義:
path 數(shù)組來表示模塊的層級關(guān)系,如果是根模塊,傳入的 path 為 [],如果傳入 [a, c],說明當前處理模塊名為 c,模塊層級為:根模塊 > a模塊 > c模塊
接下來剖析代碼:
path.slice(0, -1) 返回去除了最后一個元素的數(shù)組(注意:數(shù)組本身不會被修改),這個數(shù)組剩下的元素其實就是當前處理模塊的祖先模塊們,將這個數(shù)組進行 reduce 處理,累計值初始為 state(也就是 store.state),最后返回父鏈。
舉個栗子:
如果 path 為 [],即當前處理模塊為根模塊,經(jīng)過 reduce 后返回 state
如果 path 為 [a, c],經(jīng)過 reduce 后返回 state.a,最終 c 模塊的 state 會被掛載在 state.a.c.state 上面
掛載代碼如下:
Vue.set(parent, path[path.length - 1], module.state)
對于上面的例子,即:Vue.set(state.a, 'c', c.state)
另外,使用 Vue.set 是為了保證數(shù)據(jù)響應(yīng)式。
以上,我們的簡易版 Vuex 就實現(xiàn)完了。
另外,值得一提的是:在我們的代碼中,直接通過 let modules = options 取得了根模塊,而在 Vuex 源碼中實際還有一個模塊收集的過程,這個方法會將模塊收集成一個如下的樹結(jié)構(gòu)
{
_raw: {...},
_children: {...},
state: {...}
}
_raw 表示 options 傳入的模塊的原生形式,_children 中包含了該模塊的子模塊,state 為該模塊的 state 狀態(tài)
感興趣的話可以翻閱 Vuex 源碼,或者看看筆者的實現(xiàn)。
感謝閱讀,若以上講述有所紕漏還望指正。