手把手講解:Vuex 剖析與簡單實現(xiàn)

更多個人博客:https://github.com/zenglinan/blog


目錄

  1. install 方法
  2. 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)該是一個對象,上面有至少兩個屬性:installStoreinstall 方法在 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 被外部直接修改。

另外要注意的一個點是:我們借用 Vuestate 變成響應(yīng)式數(shù)據(jù)了。這樣的話,當 state 變化的時候,依賴的地方都能得到更新通知

以上,我們簡單實現(xiàn)了 State 狀態(tài)

(2) Getters

接下來實現(xiàn) GettersGetters 是通過 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、mutationsactions,都被定義到 store 上了,通過形如 store.getters.xxx 這種方式訪問

好了,現(xiàn)在目標明確了,接下來就把上面提到的兩個點實現(xiàn):

首先,我們需要將 storegetters、mutationsactions 進行初始化成空對象

我們不再需要用 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ù):

  1. store 實例
  2. store 上的 state
  3. path:在 installModule 中需要遞歸來安裝處理子模塊,所以用 path 數(shù)組來表示模塊的層級關(guān)系。數(shù)組的最后一位為當前要處理的模塊的模塊名,最后一位前面的都是當前模塊的祖先模塊。舉個栗子:如果是根模塊,傳入的 path[],如果傳入 [a, c],說明當前處理模塊名為 c,模塊層級為:根模塊 > a模塊 > c模塊
  4. 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.xxxstore.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)。

感謝閱讀,若以上講述有所紕漏還望指正。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 安裝 npm npm install vuex --save 在一個模塊化的打包系統(tǒng)中,您必須顯式地通過Vue.u...
    蕭玄辭閱讀 3,054評論 0 7
  • vuex 場景重現(xiàn):一個用戶在注冊頁面注冊了手機號碼,跳轉(zhuǎn)到登錄頁面也想拿到這個手機號碼,你可以通過vue的組件化...
    sunny519111閱讀 8,168評論 4 111
  • ### store 1. Vue 組件中獲得 Vuex 狀態(tài) ```js //方式一 全局引入單例類 // 創(chuàng)建一...
    蕓豆_6a86閱讀 794評論 0 3
  • 前言 之前幾篇解析 Vue 源碼的文章都是完整的分析整個源碼的執(zhí)行過程,這篇文章我會將重點放在核心原理的解析,不會...
    心_c2a2閱讀 1,568評論 1 8
  • 上一章總結(jié)了 Vuex 的框架原理,這一章我們將從 Vuex 的入口文件開始,分步驟閱讀和解析源碼。由于 Vuex...
    你的肖同學閱讀 1,898評論 3 16

友情鏈接更多精彩內(nèi)容