今天我们一起来从零编写一个redux。
redux可分为两部分,一部分讲解了store的逻辑,一部分是中间件的逻辑
store
store的主要作用是声明一个闭包对象,闭包对象中储存着我们需要存储的数据(至于它为什么是一个闭包,原因是防止外部可以直接拿到这个对象改变里面的数据),例如 { name: 'Tom', age: 20 },需要设计一个生成store的接口,如下:
const state = { // 这里我们初始化一个初始数据对象
name: 'Tom',
age: 20
}
const store = createStore(state)
// createStore方法将接收一个初始数据对象,返回一个store对象,
// store对象将暴露获取当前state数据的方法,如下:
const currentState = store.getState()
store有了,我们要有操作store的能力,根据flux思想,我们不可以直接操作store,需要通过action来更改store,action就是一个对象,描述了操作方式跟操作附带的信息,例如我们要把store中的name名字改成John,我们可以定义这样一个action:
{
type:'change_name',
payload: 'John'
}
这样,当store接收到这个action的时候,知道了这是要改名字属性,而且还知道了要将名字改成John,store就可以更新自己了。
那store要怎么接受这个action呢,显而易见,我们要设计一个接口, 如下:
const action = {
type:'change_name',
payload: 'John'
}
store.dispatch(action)
// 此为更新数据方法,此方法将返回action对象
dispatch即分发,是不是见名知意。此处dispatch为啥返回传入的action,在加载中间件中间件时会有用到,此处暂且不表。这样的话,是不是我们的存储机制是不是复合了flux的思想,获取跟更新只能通过具体的接口来实现,无法随意更改仓库数据了呢。
store的基本功能已经具备,似乎这样store的意义并不大,我们赋予它更强的能力:我们可以订阅store的变化,每次store变化时,store都能下发通知给订阅的函数,具体API设计如下:
store.subscribe(function () {
console.log('store 更新了!!!')
})
这样每次更新store都会打印
'store 更新了!!!'。
综上,我们设计涵盖了store相关的所有API,汇总如下:
import { createStore } from '你的包' // 从你的包引入创建store的API
// 声明一个store
const state = {
name: 'Tom',
age: 20
}
const store = createStore(state) // 获取最新状态
// 订阅store变化
store.subscript(function () {
console.log('store 更新了!!!')
})
// 通过action改变store
const action = {
type:'change_name',
payload: 'John'
}
store.dispatch(action)
// 获取更新后的store
const newState = store.getState() // 返回 { name: 'John', age: 20 }
设计有了,现在开始实现它:
首先要注意的是,我们拿到store不能直接改变store中的数据,只能通过dispatch。想到了,我们可以借助函数闭包,在函数中创建state变量储存仓库数据,return一个dispatch方法来改变state。相信小伙伴们都能信手拈来了,具体实现如下:
function createStore (initState = {}) {
const _state = initState
return {
dispatch: function (action) {
if (action.type === 'change_name') {
_state.name = action.payload
}
},
getState: function () {
return _state
}
}
}
const initState = {
name: 'Tom',
age: 20
}
const store = createStore(initState)
store.dispatch({
type: 'change_name',
payload: 'John'
})
store.getState() // 返回 { name: 'John', age: 20 }
我们通过闭包成功的隔离了用户可直接操作state的方式。此处还有个问题,action肯定是外部定义的,跟所存state的数据结构是有关联性的,所以我们需要在外部定义好state,action,及二者的关系。将其传入create中。这里redux发明了reducer,其实就是一种state跟action的对应关系函数。我们来看下:
const state = {
name: 'Tom',
age: 20
}
const action = {
type:'change_name',
payload: 'John'
}
function reducer (state, action) {
switch (action.type) {
case 'change_name':
return Object.assign({}, state, { name: action.payload })
case 'add_age':
return Object.assign({}, state, { age: state.age + 1 })
default:
return state
}
}
这样我们在外部声明的reducer已经包含了state,action及二者的对应关系,我们将这个reducer以及要执行的操作action传入createStore,在createStore中调用下reducer不久可以解决这个问题了么,具体代码实现如下:
// 声明state,reducer,见上面代码块,此处略
// ...
function createStore (reducer, state) { // 这里state值初始化的state对象,可以不传
const _state = {}
const getState // ...略
const subscript // ...略
const dispatch = function (action) {
_state = reducer(state || _state, action)
}
dispatch({ type: 'init_action_type' }) // 初始化store的时候我们会先调用一下这个方法,通过一个初始化type:‘init_action_type’,来调用reducer,返回默认的state
return {
getState,
subscript,
dispatch
}
}
好了,我们已经实现了外部传入初始化state,及action来通过dispatch action的方式改变state了,外部也改不了这个state,接下来实现订阅的方法:
总体思路是:我们从createStore中声明一个事件队列的数组对象handleArr = [], 每次调用store.subscribe(function() {}), 我们都将函数参数推入这个数组对象,当触发dispatch时,我们遍历数组,并执行里面的每一个函数即可。实现:
function createStore (reducer, action) {
// 省略...
const currentListeners = [] // 即handleArr
const subscribe(listener) {
currentListeners.push(listener)
return function unsubscribe () { // 此处返回一个取消订阅方法,订阅后还能取消订阅
const index = currentListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
const dispatch = function (action) {
_state = reducer(state || _state, action)
for (let i = 0; i < currentListeners.length; i++) {
const listener = currentListeners[i]
listener()
}
}
}
至此,store的基础方法都已实现。
applyMiddleware
此处进入重点,理解起来没store那么简单,请集中精神
设计初衷是我们可以在store中添加若干中间件,中间件可以在dispatch(更新数据)时获取到更新数据前后的状态并依次执行,中间件函数本身接收store跟next参数(即为dispatch方法),通过返回
首先 ,我们需要设想下中间件应该要实现一下的几点功能:
1.能在每次更新数据(dispatch)的时候拿到数据之前以及之后的数据状态。
2.中间件自己无法控制数据的更新(即调用dispatch方法),否则我可以写个中间件拦截所有的更新(阻止dispatch的执行或者自己dispatch一个随意的action)就不好了,数据更新的权限永远只能在使用中间件的用户手中,而不是编写中间件的开发者手中
3.我希望可以以数组的方式传入多个中间件,每个中间件都可以在每次数据更新(dispatch)时拿到数据更新前后的状态。
梳理下要实现的功能,很容易能想到我们需要在更新数据(即调用dispatch)前先依次调用传入的中间件,执行中间件数据前置的代码,更新数据后调用所有中间件的数据后置代码。执行流程如下图:
上图说明了多个中间件的执行流程,其实就是一个洋葱模型,上层通过调用next方法(即dispatch)来触发下一个中间件,next方法将中间件执行串联了起来。
好像理解的不是很好,我们再放一段官方编写中间件的示例代码,帮助理解下:
// 此处声明了一个中间件 logger,此中间件作用是在每次更新数据前打印触发更新的action,更新后打印下最新的state
const logger = store => next => action => { // 这里使用箭头函数嵌套返回多个函数,使用了函数柯里化的技巧,感兴趣的同学可以自己研究下
console.log('dispatching', action) // 此处打印更新前action
let result = next(action) // 此处将更新方法传递给下一个中间件,此中间件之后的中间件
console.log('next state', store.getState()) // 此处打印更新之后的state
return result
}
如果实现此功能,我们必须要把dispatch方法传入到中间件中,所以redux的作者自己编写了一个接受中间件的API,applyMiddleware,我们直接来看redux作者是怎么实现这个方法的吧
function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args) // 这里当用户调用dispatch方法时,则会触发上面的报错。
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch) // 划重点,这里compose接收一个函数数组,方法用例:compose(f, g, h) 将转换成 (...args)=> f(g(h(...args)))
return {
...store,
dispatch
}
}
}
我们这样来使用
const store = createStore(reducer, applyMiddleware([middleware1, middleware2, middleware3, ...]))
createStore内部拿到中间件执行结果会这么调用
function createStore(reducer, enhance) {// enhance 即为applyMiddleware的执行结果
// '''
if (typeof enhance === 'function' && typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer)
}
// '''
}
这里可以看出 applyMiddleware()方法的执行结果将会付给enhance, 那么enhance(createStore)结果是:
(...arg) => {
// ...
return {
...store,
dispatch
}
}
那么 enhance(createStore)(reducer)结果是:
{
...store,
dispatch
}
这里store就是通过createStore(reducer)方法生成的store,我们具体看一下这里返回的dispatch是啥。具体看以下几行代码
function applyMiddleware(...middlewares) {
// ...
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
// 这里假设只有2个中间件
// 这里看出dispatch等于: (...arg) => chain1(chain2(...arg))(store.dispatch)
// 即为 (store.dispatch) => chain1(chain2(store.dispatch))
return {
...store,
dispatch
}
// ...
}
因为chain为中间件柯里化后第二层方法,见下图next入参的函数
这里,可以知道applyMiddle返回的dispatch是中间件函数串联的集合。有koa内味了是不是。中间件串联先执行next之前的方法,更新数据后,执行之后的方法。
到此,整个流程讲完了,是不是很精妙。
前面有个伏笔,无中间件的dispatch为什么会返回一个action对象,仔细看看上面的代码,你是否知道了呢?这里希望读者能自己看看redux源码找找其中原由。
另外,很多地方为了简化理解过程对源码做了修改,勿喷
如果觉得不错,点个赞再走吧