vuex實現及簡略解析

weixin_33807284發表於2019-02-28

大家都知道vuexvue的一個狀態管理器,它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。先看看vuex下面的工作流程圖


通過官方文件提供的流程圖我們知道,vuex的工作流程,

  • 1、資料從state中渲染到頁面;
  • 2、在頁面通過dispatch來觸發action
  • 3、action通過呼叫commit,來觸發mutation
  • 4、mutation來更改資料,資料變更之後會觸發dep物件的notify,通知所有Watcher物件去修改對應檢視(vue的雙向資料繫結原理)。

使用vuex

理解vuex的工作流程我們就看看vuexvue中是怎麼使用的。

首先用vue-cli建立一個專案工程,如下圖,選擇vuex,然後就是一路的Enter鍵

安裝好之後,就有一個帶有vuexvue專案了。

進入目錄然後看到,src/store.js,在裡面加了一個狀態{count: 100},如下

import Vue from 'vue'
import Vuex from 'vuex' // 引入vuex

Vue.use(Vuex) // 使用外掛

export default new Vuex.Store({
  state: {
    count: 100 // 加一個狀態
  },
  getter: {
  
  },
  mutations: {
  
  },
  actions: {
  
  }
})

最後在App.vue檔案裡面使用上這個狀態,如下

<template>
  <div id="app">
    這裡是stort------->{{this.$store.state.count}}
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

<style>
</style>

專案跑起來就會看到頁面上看到,頁面上會有100了,如下圖

到這裡我們使用vuex建立了一個store,並且在我們的App元件檢視中使用,但是我們會有一些列的疑問。

  • store是如何被使用到各個元件上的??
  • 為什麼state的資料是雙向繫結的??
  • 在元件中為什麼用this.$store.dispch可以觸發storeactions??
  • 在元件中為什麼用this.$store.commit可以觸發storemutations??
  • ....等等等等

帶著一堆問題,我們來自己實現一個vuex,來理解vuex的工作原理。

安裝並使用store

src下新建一個vuex.js檔案,然後程式碼如下

'use strict'

let Vue = null

class Store {
  constructor (options) {
    let { state, getters, actions, mutations } = options
  }
}
// Vue.use(Vuex)
const install = _Vue => {
  // 避免vuex重複安裝
  if (Vue === _Vue) return
  Vue = _Vue
  Vue.mixin({
    // 通過mixins讓每個元件例項化的時候都會執行下面的beforeCreate
    beforeCreate () {
      // 只有跟節點才有store配置,所以這裡只走一次
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else if (this.$parent && this.$parent.$store) { // 子元件深度優先 父 --> 子---> 孫子
        this.$store = this.$parent.$store
      }
    }
  })
}

export default { install, Store }

然後修改store.js中的引入vuex模組改成自己的vuex.js

import Vuex from './vuex' // 自己建立的vuex檔案

在我們的程式碼中export default { install, Store }匯出了一個物件,分別是installStore

install的作用是,當Vue.use(Vuex)就會自動呼叫install方法,在install方法裡面,我們用mixin混入了一個beforeCreate的生命週期的鉤子函式,使得當每個元件例項化的時候都會呼叫這個函式。

beforeCreate中,第一次根元件通過store屬性掛載$store,後面子元件呼叫beforeCreate掛載的$store都會向上找到父級的$store,這樣子通過層層向上尋找,讓每個元件都掛上了一個$store屬性,而這個屬性的值就是我們的new Store({...})的例項。如下圖

通過層層向上尋找,讓每個元件都掛上了一個$store屬性

設定state響應資料

通過上面,我們已經從每個元件都通過this.$store來訪問到我們的store的例項,下面我們就編寫state資料,讓其變成雙向繫結的資料。下面我們改寫store

class Store {
  constructor (options) {
    let { state, getters, actions, mutations } = options // 拿到傳進來的引數
    this.getters = {}
    this.mutations = {}
    this.actions = {}
    // vuex的核心就是借用vue的例項,因為vuex的資料更改回更新檢視
    this._vm = new Vue({
      data: {
        state
      }
    })
  }
  // 訪問state物件時候,就直接返回響應式的資料
  get state() { // Object.defineProperty get 同理
    return this._vm.state
  }
}

傳進來的state物件,通過new Vue({data: {state}})的方式,讓資料變成響應式的。當訪問state物件時候,就直接返回響應式的資料,這樣子在App.vue中就可以通過this.$store.state.count拿到state的資料啦,並且是響應式的呢。

編寫mutations、actions、getters

上面我們已經設定好state為響應式的資料,這裡我們在store.js裡面寫上mutations、actions、getters,如下

import Vue from 'vue'
import Vuex from './vuex' // 引入我們的自己編寫的檔案

Vue.use(Vuex) // 安裝store
// 例項化store,引數數物件
export default new Vuex.Store({
  state: {
    count : 1000
  },
  getters : {
    newCount (state) {
      return state.count + 100
    }
  },
  mutations: {
    change (state) {
      console.log(state.count)
      state.count += 10
    }
  },
  actions: {
    change ({commit}) {
      // 模擬非同步
      setTimeout(() => {
        commit('change')
      }, 1000)
    }
  }
})

配置選項都寫好之後,就看到getters物件裡面有個newCount函式,mutationsactions物件裡面都有個change函式,配置好store之後我們在App.vue就可以寫上,dispatchcommit,分別可以觸發actionsmutations,程式碼如下

<template>
  <div id="app">
    這裡是store的state------->{{this.$store.state.count}} <br/>
    這裡是store的getter------->{{this.$store.getters.newCount}} <br/>
    <button @click="change">點選觸發dispach--> actions</button>
    <button @click="change1">點選觸發commit---> mutations</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  methods: {
    change () {
      this.$store.dispatch('change') // 觸發actions對應的change
    },
    change1 () {
      this.$store.commit('change') // 觸發mutations對應的change
    }
  },
  mounted () {
    console.log(this.$store)
  }
}
</script>

資料都配置好之後,我們開始編寫store類,在此之前我們先編寫一個迴圈物件工具函式。

const myforEach = (obj, callback) => Object.keys(obj).forEach(key => callback(key, obj[key]))
// 作用:
// 例如{a: '123'}, 把物件的key和value作為引數
// 然後就是函式執行callback(a, '123')

工具函式都準備好了,之後,下面直接縣編寫gettersmutationsactions的實現

class Store {
  constructor (options) {
    let { state = {}, getters = {}, actions = {}, mutations = {} } = options
    this.getters = {}
    this.mutations = {}
    this.actions = {}
    // vuex的核心就是借用vue的例項,因為vuex的資料更改回更新檢視
    this._vm = new Vue({
      data: {
        state
      }
    })
    // 迴圈getters的物件
    myforEach(getters, (getterName, getterFn) => {
      // 對this.getters物件進行包裝,和vue的computed是差不多的
      // 例如 this.getters['newCount'] = fn(state)
      // 執行 this.getters['newCount']()就會返回計算的資料啦
      Object.defineProperty(this.getters, getterName, {
        get: () => getterFn(state)
      })
    })
    // 這裡是mutations各個key和值都寫到,this.mutations物件上面
    // 執行的時候就是例如:this.mutations['change']()
    myforEach(mutations, (mutationName, mutationsFn) => {
      // this.mutations.change = () => { change(state) }
      this.mutations[mutationName] = () => {
        mutationsFn.call(this, state)
      }
    })
    // 原理同上
    myforEach(actions, (actionName, actionFn) => {
      // this.mutations.change = () => { change(state) }
      this.actions[actionName] = () => {
        actionFn.call(this, this)
      }
    })
    const {commit , dispatch} = this // 先存一份,避免this.commit會覆蓋原型上的this.commit
    // 解構 把this繫結好
    // 通過結構的方式也要先呼叫這類,然後在下面在呼叫原型的對應函式
    this.commit = type => {
      commit.call(this, type)
    }
    this.dispatch = type => {
      dispatch.call(this, type)
    }
  }
  get state() { // Object.defineProperty 同理
    return this._vm.state
  }
  // commi呼叫
  commit (type) {
    this.mutations[type]()
  }
  // dispatch呼叫
  dispatch (type) {
    this.actions[type]()
  }
}

通過上面的,我們可以看出,其實mutationsactions都是把傳入的引數,賦值到store例項上的this.mutationsthis.actions物件裡面。

當元件中this.$store.commit('change')的時候 其實是呼叫this.mutations.change(state),就達到了改變資料的效果,actions同理。

getters是通過對Object.defineProperty(this.getters, getterName, {})
對this.getters進行包裝當元件中this.$store.getters.newCount其實是呼叫getters物件裡面的newCount(state),然後返回計算結果。就可以顯示到介面上了。

大家看看完成後的效果圖。

到這裡大家應該懂了vuex的內部程式碼的工作流程了,vuex的一半核心應該在這裡了。為什麼說一半,因為還有一個核心概念module,也就是vuex的資料的模組化。

vuex資料模組化

由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的物件。當應用變得非常複雜時,store 物件就有可能變得相當臃腫。

為了解決以上問題,Vuex 允許我們將 store 分割成模組(module)。每個模組擁有自己的 state、mutation、action、getter、甚至是巢狀子模組——從上至下進行同樣方式的分割

例如下面的store.js

// 例項化store,引數數物件
export default new Vuex.Store({
  modules: {
    // 模組a
    a: {
      state: {
        count: 4000
      },
      actions: {
        change ({state}) {
          state.count += 21
        }
      },
      modules: {
        // 模組b
        b: {
          state: {
            count: 5000
          }
        }
      }
    }
  },
  state: {
    count : 1000
  },
  getters : {
    newCount (state) {
      return state.count + 100
    }
  },
  mutations: {
    change (state) {
      console.log(state.count)
      state.count += 10
    }
  },
  actions: {
    change ({commit}) {
      // 模擬非同步
      setTimeout(() => {
        commit('change')
      }, 1000)
    }
  }
})

然後就可以在介面上就可以寫上this.$store.state.a.count(顯示a模組count)this.$store.state.a.b.count(顯示a模組下,b模組的count),這裡還有一個要注意的,其實在元件中呼叫this.$store.dispatch('change')會同時觸發,根的actionsa模組actions裡面的change函式。

下面我們就直接去實現models的程式碼,也就是整個vuex的實現程式碼,

'use strict'

let Vue = null
const myforEach = (obj, callback) => Object.keys(obj).forEach(key => callback(key, obj[key]))

class Store {
  constructor (options) {
    let state = options.state
    this.getters = {}
    this.mutations = {}
    this.actions = {}
    // vuex的核心就是借用vue的例項,因為vuex的資料更改回更新檢視
    this._vm = new Vue({
      data: {
        state
      }
    })

    // 把模組之間的關係進行整理, 自己根據使用者引數維護了一個物件
    // root._children => a._children => b
    this.modules = new ModulesCollections(options)
    // 無論子模組還是 孫子模組 ,所有的mutations 都是根上的
    // 安裝模組
    installModules(this, state, [], this.modules.root)

    // 解構 把this繫結好
    const {commit , dispatch} = this
    // 通過結構的方式也要先呼叫這類,然後在下面在呼叫原型的對應函式
    this.commit = type => {
      commit.call(this, type)
    }
    this.dispatch = type => {
      dispatch.call(this, type)
    }
  }
  get state() { // Object.defineProperty 同理
    return this._vm.state
  }
  commit (type) {
    // 因為是陣列,所以要遍歷執行
    this.mutations[type].forEach(fn => fn())
  }
  dispatch (type) {
    // 因為是陣列,所以要遍歷執行
    this.actions[type].forEach(fn => fn())
  }
}

class ModulesCollections {
  constructor (options) { // vuex []
    // 註冊模組
    this.register([], options)
  }
  register (path, rawModule) {
    // path 是空陣列, rawModule 就是個物件
    let newModule = {
      _raw: rawModule, // 物件
      _children: {}, // 把子模組掛載到這裡
      state: rawModule.state
    }
    if (path.length === 0) { // 第一次
      this.root = newModule
    } else {
      // [a, b] ==> [a]
      let parent = path.slice(0, -1).reduce((root, current) => {
        return root._children[current]
      }, this.root)
      parent._children[path[path.length - 1]] = newModule
    }
    if (rawModule.modules) {
      // 遍歷註冊子模組
      myforEach(rawModule.modules, (childName, module) => {
        this.register(path.concat(childName), module)
      })
    }
  }
}

// rootModule {_raw, _children, state }
function installModules (store, rootState, path, rootModule) {
  // rootState.a = {count:200}
  // rootState.a.b = {count: 3000}
  if (path.length > 0) {
    // 根據path找到對應的父級模組
    // 例如 [a] --> path.slice(0, -1) --> []  此時a模組的父級模組是跟模組
    // 例如 [a,b] --> path.slice(0, -1) --> [a]  此時b模組的父級模組是a模組
    let parent = path.slice(0, -1).reduce((root, current) => {
      return root[current]
    }, rootState)
    // 通過Vue.set設定資料雙向繫結
    Vue.set(parent, path[path.length - 1], rootModule.state)
  }
  // 設定getter
  if (rootModule._raw.getters) {
    myforEach(rootModule._raw.getters, (getterName, getterFn) => {
      Object.defineProperty(store.getters, getterName, {
        get: () => {
          return getterFn(rootModule.state)
        }
      })
    })
  }
  // 在跟模組設定actions
  if (rootModule._raw.actions) {
    myforEach(rootModule._raw.actions, (actionName, actionsFn) => {
      // 因為同是在根模組設定,子模組也有能相同的key
      // 所有把所有的都放到一個陣列裡面
      // 就變成了例如 [change, change] , 第一個是跟模組的actions的change,第二個是a模組的actions的change
      let entry = store.actions[actionName] || (store.actions[actionName] = [])
      entry.push(() => {
        const commit = store.commit
        const state = rootModule.state
        actionsFn.call(store, {state, commit})
      })
    })
  }
  // 在跟模組設定mutations, 同理上actions
  if (rootModule._raw.mutations) {
    myforEach(rootModule._raw.mutations, (mutationName, mutationFn) => {
      let entry = store.mutations[mutationName] || (store.mutations[mutationName] = [])
      entry.push(() => {
        mutationFn.call(store, rootModule.state)
      })
    })
  }
  // 遞迴遍歷子節點的設定
  myforEach(rootModule._children, (childName, module) => {
    installModules(store, rootState, path.concat(childName), module)
  })
}

const install = _Vue => {
  // 避免vuex重複安裝
  if (Vue === _Vue) return
  Vue = _Vue
  Vue.mixin({
    // 通過mixins讓每個元件例項化的時候都會執行下面的beforeCreate
    beforeCreate () {
      // 只有跟節點才有store配置
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else if (this.$parent && this.$parent.$store) { // 子元件深度優先 父 --> 子---> 孫子
        this.$store = this.$parent.$store
      }
    }
  })
}

export default { install, Store }

看到程式碼以及註釋,主要流程就是根據遞迴的方式,處理資料,然後根據傳進來的配置,進行運算元據。

至此,我們把vuex的程式碼實現了一遍,在我們App.vue的程式碼裡新增

<template>
  <div id="app">
    這裡是store的state------->{{this.$store.state.count}} <br/>
    這裡是store的getter------->{{this.$store.getters.newCount}} <br/>
    這裡是store的state.a------->{{this.$store.state.a.count}} <br/>
    <button @click="change">點選觸發dispach--> actions</button>
    <button @click="change1">點選觸發commit---> mutations</button>
  </div>
</template>

最後檢視結果。

完結撒花~~~

部落格文章地址:https://blog.naice.me/article...

原始碼地址:https://github.com/naihe138/w...

相關文章