透过Redux4.0源码,结合工作中的案例聊聊中间件技术的应用

关于源码的分析有很多。但大多是一上来直接贴出代码,然后一行一行加注释。

当然这样也没啥毛病,函数简单的还好,如果函数复杂,你就在短时间内很难在脑海中反推出各种输入输出情况,以至于看到后面想睡觉。

当我们阅读一个已经沉淀了好几年的,并且拥有大量开发实践的库时,透过作者的测试用例看源码会是一个比较好的方式。

这样,它立马就会让你知道某个函数干了啥,各种输入输出用例是怎样的。

优秀的库,测试代码一般都比功能代码多。

在我看来目前redux中间件就是函数式编程中的包菜式组合a(b(c(d(...args)))),其实就是KOA中的经典的洋葱圈模型。

为什么会有中间件这么一说,在我看来就是由于业务灵活多变,导致原有的函数不能满足业务的需求,需要增强。

当然这个增强并不是直接修改原有的函数代码(这样就太low了,而且也不易维护和扩展)。

而是通过函数式编程(functional programming)中的提倡的高阶函数来增强的。

在开始“”之前,我们先看看常见的函数式编程模型几个概念(redux这个库就是函数式编程的典范)

  1. 闭包(Closure)>>>保留局部变量不会被释放的代码块,称之为闭包。
  2. 高阶函数(Higher-order function) >>>接受或返回一个函数的函数称为高阶函数。
  3. 柯里化(Currying)>>>给一个函数的传递部分参数,返回一个接受其他参数的新函数。
  4. 组合(Composing)>>>将多个函数的能力合并,返回一个新的函数。
(大家如果对这几个概念不是特别清楚,可以自行google先了解下。当然也可以直接往下看,也没啥大问题。)

言归正传,既然是由浅入深的“聊”,那么就由一个实际工作中的问题来入手。

工作中我们常看到这样的代码:

function success(state) {
    if(state.code == 200) {
        //bulalalla一堆处理逻辑
        return;
    }
    if(state.code == 411) { 
        //bulalalla一堆处理逻辑
        return;    
    }
    // 然后某一天,因为业务需求,又要针对412状态码的错误提示进行特殊指引和提示
    // 然后某一天,因为业务需求,又要针对413状态码的错误提示进行特殊指引和提示
    // bulalala……这样一直修改success函数本身式的增强,如果一个不小心手一抖,改错了,嘿嘿~~
    // 在这里,我们优雅的方式是,我们希望状态码的处理方式可插拔的,随意组合等。
    // 中间件技术能帮我们搞定最这一切
    throw Error(state.code);
}

$.getJson('xx.com/api', success);复制代码

接下来我们用高阶函数来增强它,而不是修改其本身。

这些个高阶函数就是我们中间件的雏形。

function success(data) {
    return data; // 不做任何code处理
}

// code等于200的处理的高阶函数(接收一个函数作为参数,并返回一个新的函数)
function code200(func) {
    return function(...args) {
        if(data.code === 200){
            //.....code等于200的处理
        } else {
            func(...args);
        }
    }
}
// code200(success) --->> 瞬间就让success具有了处理状态码200的能力
$.getJson('xx.com/api', code200(success));

// 再增加一个code等于411的处理
function code411(func) {
     return function(...args) {
        if(data.code === 411){
            //.....code等于411的处理
        } else {
            func(...args);
        }
    }
}
// code411(code200(success)) --->> 瞬间就让success函数具有了处理状态码411, 200的能力
$.getJson('xx.com/api', code411(code200(success)));复制代码

有些同学可能不太懂这个code411(code200(success))是个什么鬼?

//这里我整理一下
code411(code200(success)) 
//就等于===
function code411(...args) {
    if(data.code === 411){
        //.....code等于411的处理
    } else {
      code200(...args); // 通过高阶函数缓存的函数引用func -> code200
    }
}
function code200(...args) {
    if(data.code === 200){
        //.....code等于200的处理
    } else {
      success(...args); // 通过高阶函数缓存的函数引用func -> success
    }
}
function success(data) {
    return data; // 不做任何处理
}

复制代码

到此为止,就形成了一个洋葱圈模型的middlewares的链条。

每次这样去写a(b(c(d(e(origin)))))这样的玩意儿,实在是恶心,所以我们应该用一个函数来帮我们做这件事。

OK,我觉得我现在可以贴上Reduxcompose API的源码和测试用例来分析了^_^……


大家看到没,compose功能代码才10行,而它的测试用例代码居然有整整52行。

透过测试用例,我们清晰的知道compose代码输入和输出,那么研究起源码来就轻松多了(至少在心理上?)。

在分析这段代码之前,我先看看MDN对reduce函数的解释。


是不是,瞬间秒懂。

接下来我们就来分析下componse API中的reduce是如何工作的。

Redux  compose 函数源码

export default function compose(...funcs) { 
    if (funcs.length === 0) {    
        return arg => arg  
    }
    if (funcs.length === 1) {
        return funcs[0]
    }
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}复制代码
Redux compose  函数 'composes functions from right to left' 测试用例代码

it('composes functions from right to left', () => { 
    const a = next => x => next(x + 'a')      
    const b = next => x => next(x + 'b')   
    const c = next => x => next(x + 'c')   
    const final = x => x
    expect(compose(a, b, c)(final)('')).toBe('abc')   
    expect(compose(b, c, a)(final)('')).toBe('bca') 
    expect(compose(c, a, b)(final)('')).toBe('cab')  
})复制代码

透过上面代码,我们清晰的看到了输入与输出。

调用compose(a, b, c)会执行funcs.reduce((a, b) => (...args) => a(b(...args)))

然后,会返回一个创建中间件链的函数(...args) => a(b(c(...args)))

在这里,为什么会返回(...args) => a(b(c(...args))),我们详细分析下:

  • funcs.reduce((a, b) => (...args) => a(b(...args)))
    1. prev1 =  (...args) => a(b(...args))
    2. res = (...args) => prev1(c(...args))
此时,我们把 c(...args)看成一个参数传递给 prev1,会变成 a(b(c(...args)))

那么最终res 结果就是(...args) => a(b(c(...args)))

接下我们执行res(final)创建创建中间件链

其实就是执行a(b(c(final))),这里的final就是我们要增强的函数。a(b(c(final)))执行流程就是上面的code411(code200(success))(已做分析)。

所以a(b(c(final)))返回一个被增强了的final函数,我们在这里定义它为final2

所以最后函数执行应该是这个样子的:

  1. final2('')
  2. next('' + 'a')
  3. next('a' + 'b')
  4. next('ab' + 'c')
  5. 'abc'

以上Redux洋葱圈模型中间件技术核心(按传入的顺序,把各个中间件函数链接起来,然后顺序执行来增强原有函数的处理能力)。

掌握好它,在项目中扩展业务处理函数将会变得非常容易和清晰可维护。

理解了compose函数后,我们来看看Redux提供的applyMiddleware函数就会变得简单很多。

同样,我们可以透过测试用例看源码。

这是一段典型的函数式编程代码。它是闭包,高阶函数,柯里化,组合等等这些常见的函数式编程模型的综合运用。

我在前面说过,中间件函数你可以理解为其实就是在不改变原来函数代码的情况下,增强原来函数的处理能力。

透过let dispatch = store.dispatchdispatch = compose(...chain)(store.dispatch)我们清晰的看到函数并没有直接给里面的store.dispatch赋值,而是通过compose组合[中间件函数]和[它本身],来返回一个增强版的dispatch函数。函数最后返回给用户一个全新的store单例(return {...store,dispatch})。

所以,我们用户执行store.dispatch就会经过一堆函数进行逻辑处理。

附上张中间件执行图:




最后,祝大家生活愉快!

你可能感兴趣的