從零實現支持洋蔥模型中間件的 vuex

前言

剛開始看 redux 時候,reducer、store、dispatch、middleware 這些名詞都比較難以理解,後面接觸了 vuex 就比較好理解了。本章會從零開始實現一個簡單版本的狀態管理器。方便大家今後理解 vuex 和 redux 的狀態管理庫的源碼

什麼是狀態管理器

一個狀態管理器的核心思想是將之前放權到各個組件的修改數據層的 controller 代碼收歸一處,統一管理,組件需要修改數據層的話需要去觸發特定的預先定義好的 dispatcher,然後 dispatcher 將 action 應用到 model 上,實現數據層的修改。然後數據層的修改會應用到視圖上,形成一個單向的數據流。

簡單的狀態管理器

本文會一步步的編寫一個 Store 類,實現狀態的同步更新、異步更新、中間件等方法。

首先,我們編寫一個 Store 類。

class Store {
  constructor({ state, mutations, actions }) {
    this._state = state
    this._mutations = {}
    this._actions = {}
    this._callbacks = []
  }
}

state 中存放所有需要的數據, mutaition 是更改 state 的唯一方法。 action 類似於 mutation, 不同在於,action 通過提交 mutation 來更改 state,action 可以包含任意的異步操作。 這和 vuex 是一樣的。 當我們更改 state 時,需要通知到訂閱者。這裏可以實現發佈-訂閱模式來完成。callbacks 用來存放訂閱者的回調函數。下面我們來一一實現這些方法。

Mutation

更改 Store 中 state 的唯一方法是提交 mutation,每個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。這個回調函數就是我們實際進行狀態更改的地方,並且它會接受 state 作爲第一個參數,然後對 state 進行一些更改:

const state = {
  name: 'webb',
  age: 20,
  data: {}
}

const mutations = {
  changeAge(state, data) {
    state.age = data
  },
  changeData(state, data) {
    state.data = data
  }
}

接下來我們實現把 state 作爲第一個參數傳遞給 mutation

function initMutation(state, mutations, store) {
  const keys = Object.keys(mutations)
  keys.forEach(key => {
    registerMutation(key, mutations[key], state, store)
  })
}

function registerMutation(key, handle, state, store) {
  // 提交 mutation 時 實際執行 store._mutations[key]函數,這個函數接受一個 data 參數
  // 並且實現了把 state 作爲第一個參數傳入回調函數中
  store._mutations[key] = function(data) {
    handle.call(store, state, data)
  }
}

改造一下 Store 類

class Store {
  constructor({ state, mutations, actions }) {
    this._state = state
    this._mutations = {}
    this._actions = {}
    this._callbacks = []
  }
}

Store.prototype._init = function (state, mutations, actions) {
  initMutation(this, mutations, state)
}

Store.prototype.commit = function(type, payload) {
  const mutation = this._mutations[type]
  mutation(payload)
}

// 獲取最新 state
Store.prototype.getState = function() {
  return this._state
}

const store = new Store({
  state,
  mutations
})

通過 commit 一個 mutation 來更新 state

console.log(store.getState()) // {name: 'webb', age: 20, data: {}}

store.commit('changeAge', 21)

console.log(store.getState()) // {name: 'webb', age: 21, data: {}}

到這裏我們實現了當提交 mutation 的時候,會修改 state 的值,現在有一個問題擺在我們面前,如果直接通過 this._state.xx = xx 也是可以修改 state的值的。我們應該避免直接修改state的值。那麼我們可以在修改 state 的時候做一層攔截,如果不是 mutation 修改的,就拋出錯。現在我們嘗試用 es6 proxy 來解決這個問題。

class Store {
  constructor({ state, mutations, actions }) {
    this._committing = false  // 用來判斷是否是 commit mutation 觸發更新
    this._mutations = {}
    this._init(state, mutations, actions)
  }
}

Store.prototype._init = function (state, mutations, actions) {
  this._state = initProxy(this, state)
}

Store.prototype.commit = function(type, payload) {
  const mutation = this._mutations[type]
  this._committing = true
  mutation(payload)
  this._committing = false
}

// 對 state 的操作加入攔截,如果不是 commit mutation 就拋出錯誤
function initProxy(store,state) {
  return new Proxy(state, handle(store))
}

function handle(store) {
  return {
    get: function (target, key) {
      return Reflect.get(target, key)
    },
    set: function (target, key, value) {
      if (!store._committing) {
        throw new Error('只能通過 mutation 更改 state')
      }
      return Reflect.set(target, key, value)
    }
  }
}

Subscribe

上面我們完成了對 state 數據的修改。接下來我們實現,當 state 數據更新後,通知到相關 state 的使用者。

// 收集訂閱者
Store.prototype.subscribe = function (callback) {
  this._callbacks.push(callback)
}

// 修改 state 後, 觸發訂閱者的回調函數,並把舊的 state 和新的 state 作爲參數傳遞
Store.prototype.commit = function (mutationName, payload) {
  const mutation = this._mutations[mutationName]
  const state = deepClone(this.getStatus())
  this._committing = true
  mutation(payload)
  this._committing = false
  this._callbacks.forEach(callback => {
    callback(state, this._state)
  })
}

const store = new Store({
  state,
  mutations
})

store.subscribe(function (oldState, newState) {
  console.log('old', oldState)
  console.log('new', newState)
})

store.commit('changeAge', 21)
// old: { name: 'webb', age: 20, data: {} }
// new: { name: 'webb', age: 21, data: {} }

上面代碼中我們使用發佈-訂閱模式,通過 subscribe 函數訂閱 state 的變化,在 mutation 執行完成後,調用訂閱者的回調函數,並把之前的 state 的 最新的 state 作爲參數返回。

actions

vuex 文檔中提到

一條重要的原則就是要記住 mutation 必須是同步函數 爲什麼?請參考下面的例子:
mutations: {
 someMutation (state) {
   api.callAsyncMethod(() => {
     state.count++
   })
 }
}
現在想象,我們正在 debug 一個 app 並且觀察 devtool 中的 mutation 日誌。每一條 mutation 被記錄,devtools 都需要捕捉到前一狀態和後一狀態的快照。然而,在上面的例子中 mutation 中的異步函數中的回調讓這不可能完成:因爲當 mutation 觸發的時候,回調函數還沒有被調用,devtools 不知道什麼時候回調函數實際上被調用——實質上任何在回調函數中進行的狀態的改變都是不可追蹤的

所以爲了處理異步操作,我們需要實現 action。

const mutations = {
  changeData(state, data) {
    state.data = data
  }
}
const actions = {
  async getData({ commit }) {
    const data = await axios.get('http://ip-api.com/json')
    commit('changeData', data.data.status)
  }
}

function initAction(store, actions) {
  const keys = Object.keys(actions)

  keys.forEach(key => {
    registerAction(key, store, actions[key])
  })
}

function registerAction(key, store, handle) {
  store._actions[key] = function (data) {
    // 把 commit 和 state 作爲參數傳遞給 action 的回調函數,當異步任務執行完成後,可以 commit 一個 mutation 來更新 state
    let res = handle.call(store, { commit: store.commit.bind(store), state: store._state }, data)
    return res
  }
}

// action 通過 dispatch 來觸發, 並把更新後的 state 作爲 promise 的結果返回
Store.prototype.dispatch = function (actionName, payload) {
  return new Promise((resolve, reject) => {
    const action = this._actions[actionName]
    const self = this
    // action 異步操作返回 promise,當 promise 有結果時,獲取最新的 state 返回。
    action(payload).then(() => {
      resolve(this._state)
    })
  })
}

store.dispatch('getData').then(state => {
  console.log('dispatch success', state)
})

到這裏我們已經實現了一個基本的狀態管理器。

中間件 middleware

現在有一個需求,在每次修改 state 的時候,記錄下來修改前的 state ,爲什麼修改了,以及修改後的 state。
這裏我們模仿 koa 的洋蔥模型中間件來實現。

// 首先定義一個 middleware 類
class Middleware {
  constructor() {
    this.middlewares = []
    this.index = 0
  }

  use(middleware) {
    this.middlewares.push(middleware)
  }

  exec() {
    this.next()
  }

  next() {
    if (this.index < this.middlewares.length) {
      const middleware = this.middlewares[this.index]
      this.index++
      middleware.call(this, this.next.bind(this))
    } else {
      this.index = 0
    }
  }
}
// 每次 commit 的時候去執行中間件
Store.prototype.commit = function (mutationName, payload) {
  const mutation = this._mutations[mutationName]
  const state = deepClone(this.getStatus())
  execMiddleware(this) // 執行中間件
  this._committing = true
  mutation(payload)
  this._committing = false
  this._callbacks.forEach(callback => {
    callback(state, this._state)
  })
}

// 註冊中間件
store.$middlewares.use(async next => {
  console.log('start middleware', store.getStatus())
  await next()
  console.log('end middleware', store.getStatus())
})

store.commit('changeAge', 21)

// start middleware { name: 'webb', age: 20, data: {} }
// end middleware { name: 'webb', age: 20, data: {} }

中間件的完整代碼可以查看 github

總結

好了,到這裏一個支持中間件模式的微型狀態管理庫已經實現了。當然 vuex 的源碼比這要複雜很多,希望通過本文能讓你更好的閱讀理解 vuex 的源碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章