刚开始看 redux 时候,reducer、store、dispatch、middleware 这些名词都比较难以理解,后面接触了 vuex 就比较好理解了。本章会从零开始实现一个简单版本的状态管理器。方便你们从此理解 vuex 和 redux 的状态管理库的源码vue
一个状态管理器的核心思想是将以前放权到各个组件的修改数据层的 controller 代码收归一处,统一管理,组件须要修改数据层的话须要去触发特定的预先定义好的 dispatcher,而后 dispatcher 将 action 应用到 model 上,实现数据层的修改。而后数据层的修改会应用到视图上,造成一个单向的数据流。ios
本文会一步步的编写一个 Store 类,实现状态的同步更新、异步更新、中间件等方法。git
首先,咱们编写一个 Store 类。es6
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 用来存放订阅者的回调函数。下面咱们来一一实现这些方法。github
更改 Store 中 state 的惟一方法是提交 mutation,每一个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是咱们实际进行状态更改的地方,而且它会接受 state 做为第一个参数,而后对 state 进行一些更改:web
const state = { name: 'webb', age: 20, data: {} } const mutations = { changeAge(state, data) { state.age = data }, changeData(state, data) { state.data = data } }
接下来咱们实现把 state 做为第一个参数传递给 mutationvuex
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 类json
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 来更新 stateredux
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 来解决这个问题。axios
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) } } }
上面咱们完成了对 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 做为参数返回。
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) })
到这里咱们已经实现了一个基本的状态管理器。
如今有一个需求,在每次修改 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 的源码。