快速手寫本身的Vuex

在閱讀Vuex源碼以前,由於Vuex的api和使用功能略微複雜,默認覺得實現起來至關複雜,望而生畏。然而經過深刻學習源碼,發現核心功能結合vue實現起來很是巧妙,也就核心幾行代碼,直呼內行。本文也就100左右行代碼就能快速手寫本身的Vuex代碼!html

前言

Vuex 是⼀個專爲 Vue.js應⽤程序開發的狀態管理模式。它採⽤集中式存儲管理應⽤的全部組件的狀態,並以相應的規則保證狀態以⼀種可預測的⽅式發⽣變化。那麼Vuex和單純的全局對象有什麼不一樣呢?vue

Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發⽣變 化,那麼相應的組件也會相應地獲得⾼效更新。

不能直接改變 store 中的狀態。改變 store 中的狀態的惟⼀途徑就是顯式地提交 (commit) mutation。這樣使得咱們能夠⽅便地跟蹤每⼀個狀態的變化,從⽽讓咱們可以實現⼀些⼯具幫助我 們更好地瞭解咱們的應⽤。git

經過以上兩點認知咱們來快速實現本身的Vuex!github

Vuex 初始化

爲何在vue實例化的時候要傳入store去實例化呢?那是爲了讓vue全部的組件中能夠經過 this.$store來獲取該對象,即 this.$store 指向 store 實例。vuex

// 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)來註冊。api

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 對象的構造函數接收⼀個對象參數,它包含 actionsgettersstatemutations 等核⼼概念,接下來咱們一一實現。promise

Vuex state

其實 state 是 vue 實例中的 data ,經過 Store 內部建立Vue實例,將 state 存儲到 data 裏,而後改變 state 就是觸發了 data 數據的改變從而實現了視圖的更新。app

// 實例化 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

知道statevue實例中的data,那麼同理,getters 就是 vue中的計算屬性 computed。ide

// 實例化 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'
  }

}

mutationsactions 的實現大同小異,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行左右,可是他巧妙的結合了vuedatacomputeds屬性,化繁爲簡,實現了複雜的功能,因此說vuex是不能脫離vue而獨立運行的。
本文是結合官網源碼提取核心思想手寫本身的Vuex,而官網的Vuex,爲了不store結構臃腫,還實現了modules等功能,具體實現能夠查看Vuex官網源碼

相關文章
相關標籤/搜索