动机
- 相信应该有不少小伙伴和我一样,虽然看过redux源码,也算知道其运行原理,但是似乎每次使用起来,尤其是将一堆middleware串起来的时候还利用到compose方法,似乎可以正确的使用,但是由于没有理解透彻,总感觉依旧很绕。没错,就是很绕,这种感觉就像写递归函数,观诺兰电影,看埃舍尔的画…… 内心OS: "什么鬼",如果你有同感,那么你应该是来对了地方。
- 最近在试着总结一些常常碰到的比较绕,但是又是绕不开的知识点,试着和大家一起从那里绕出来……(略冷的笑话😂)
正文
javascript求值策略
- 对求值策略有兴趣的当然还是多查查多看看可以理解的更深,更全面,但是这篇小文中不打算列很多的概念关于什么是求值策略,js中是如何处理的等,因为那样会让我们陷入相当经典的XY问题的怪圈,不如我们直接上些简单的小段代码能有更深的体会。
const increment = num => ++num // 返回的值是输入值+1
const foo = arg => {
console.log(arg);
} // 仅仅打印输入
foo( increment(3) + 1) // 将一个计算式当作参数传入
- ⬆️上边的代码打印结果是5,也就是说在进入foo函数体之前,如果参数是计算式,会先把这个计算式得出结果。所以其实我们是不是可以这么理解:执行一个js方法,如果其参数是一个计算式--比如函数调用--那么我们会所以下边的代码应该就会清楚了
const bar = () => foo(increment(increment(1)))
bar()
- 当运行bar的时候,其运行的顺序是“从内到外”的,先执行
increment(1)
,得到的结果是2;这个结果作为第二个外层的increment的参数,然后执行外层的increment;得到的结果最后作为foo的参数,foo被执行。那么我们把bar写的再通用一些吧,毕竟函数的作用就是为了更加通用,
const bar = (num) => foo(increment(increment(num)))
let num = 1
bar(num)
- 现在的bar更加通用了,通过调用的时候给入数字,可以控制结果。但是似乎还是有些美中不足,如果我们不想用
foo
或者increment
方法了,而是任何其他的方法呢,可不可以有更加通用的形式?redux的compose方法就是答案
看看redux的compose方法
- 从上一节的
bar
方法就是非常典型的连续计算的例子,即每次的计算都依赖于上一次计算的结果。 - 多说无益,不如直接把compose的源代码粘贴过来(现在的redux都用ts写了,但是我们还是看原来的js的写法)
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)))
}
-
参数数量为0和1的情况就不多赘述了,来看参数多于1个的情况,当然涉及到reduce,可能就有点儿绕了,没关系,我们一步一步解释:
- 首先记住reduce里callback的参数第一个参是上一次循环的返回值(或者根据MDN叫它accumulator,顾名思义就是积聚的效果),第二个参是当前遍历的元素(或者根据MDN是current,即当前元素),后边的参不赘述,可以一步MDN去看。
- 我们再观察reduce的callback的返回值,其callback不论是第一次遍历还是最后一次,返回的都是一个方法,其签名的形态是
(...args) => a(b(...args))
- 现在我们先不要管之前的所有步骤,只看最后一步的返回值(2中已经写了其签名了,不再粘贴了),如果我们传入一些参数,然后执行呢,其形态是不是和上边提到的bar方法一摸一样?是的,一样,所以其实compose的成果就是生成一个‘bar’方法,但是相比bar,compose的结果更加通用,毕竟只要给入一个函数的数组就可以得到任意我们想要的组合了。
-
说了这么多,似乎又回到了原点,有的朋友肯定会说,我知道compose的作用,也知道最后得到的方法的形态,但是每次reduce的积累才是最不直观的。没错,我完全同意,不如我试着来和大家一起分析分析每一个reduce的步骤前后的accumulator和current的样子吧:
- 不如先创建3个基本函数(3个例子基本就能讲明白了),形态如下(我们先不讨论如何利用迭代简单生成一系列的雷同的函数,这样直接写出来可能更直观,毕竟也就3个)
const a = () => {console.log('a')} const b = () => {console.log('b')} const c = () => {console.log('c')}
- 然后我们使用compose得到一个方法,可以自己尝试run一下看看结果
const ret = compose(a,b,c) ret()
- 现在来尝试解析中间的每个步骤,(因为没有在reduce中给initialValue,所以第一次遍历会直接用到两个,注意以下不是实际代码,且以下涉及到的
abc
是上述定义的三个方法,不是compose中的ab),第一次返回的结果如下,并且我们不妨把这个返回结果命名为k:
let k = (...args) => a(b(...args))
- 第二次遍历其实已经是最后一次了😂,那么最后返回的方法如下
let ret = (...args) => k(c(...args))
不如先看ret的函数体,函数体中就是k的执行,而k的执行其实就是参数为c的执行的k的执行,也就是可以如下的IIFE表示
((...args)=>a(b(...args)))(c())
,所以就是a(b(c()))
5. 同理可以知道,这时如果引入了方法d,那么其实compose的结果就会是这个retWithD
let retWithD = (...args) => ret(d(...args)))
- 根据上边我们知道,这时候如果只看
retWithD
的函数体,其实就是参数为d的执行的ret的执行,可以用如下的IIFE表示((...args)=>k(c(...args)))(d())
,可以看出来,d()
会进入c的参数位置,然后k
是我们之前讨论过的,所以最终的呈现形式就是(...args)=>a(b(c(d(...args))))
那么我们在使用这个方法的时候,只要给入参数都会导致一个连锁效应,即dcba依次执行 - 总算可以和上边的bar相呼应了,也知道了是怎么来的了,接下来总算可以看看这个方法在redux 串联middleware,或者说是依次增强
dispatch
中是怎么呈现的了.
未完待续。。。
- 还没完结,先停在这里,回头继续补上。不打算沿着这个写第二篇,而是会继续编辑,私以为一篇文章就行了。