从零实现支持洋葱模型中间件的 vuex

2,362 阅读6分钟

前言

刚开始看 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 的源码。