React Hooks 极限函数式
转载自前端备忘录-江湖术士
基础函数式,比如纯函数,柯里化什么的,就先不讨论了,大家可以自行搜索,这部分文章非常多
如果形成一个标准封装的函数结构,那么我们可以做些什么事情呢?
1 | function someFunc(state) { |
当我们做了科里化之后,两个函数如果共享同一个参数和返回数据,我们可以把它们连接起来
someFunc(antherFunc(state))
这就构成了一个流水生产线,以此类推还可以叠加更多
someFunc1(someFunc2(someFunc3(someFunc4)))
通过类似 ramda 库中的 compose 封装一下,可以更加美观:
R.compose(someFunc3,someFunc2,someFunc1)()
注意顺序
这就很明显是个管道了,并且是个单向数据流:
$.pipe(someFunc3,someFunc3,someFunc1)
好了,到这里,熟悉过相关工具的人就会发现,这就是 cycle.js
或者 rxjs
的标准结构:
1 | // cycle.js |
其一: 函数式相关优势
由于 state 是统一的,并且数据流中前后函数的不变形和纯度不需要过多的人为控制,并不像类似 redux 那样,还需要你手动保持函数纯度(如果挑选适合的操作函数的话,map,tap 先不讨论)
其二:类型推断
管道函数的类型不需要你自己来手动声明,他会依次传递,到了你所在的节点,形参必然会有类型
其三:测试友好
操作函数是最小功能单位,符合函数式自底向上开发模式,不需要进行单元测试 —— 同一个单位,如果没有副作用,一但出了错误,那肯定是逻辑的问题,因为它只有入参和出参,逻辑又是确定的,稳定性又由第三方(比如微软)保证,当前开发层级中,对于连接起来的单向数据流,没有测试的必要,只需要验证功能
举个例子,debounce,startWith 是 Rxjs 操作函数,它的稳定性有微软和部分第三方开发者保证,在使用途中将其视为运算符(你会测试 1+1=2 么?),于是使用时,你相当于自己利用他们发明了一个新语言(《计算机程序的构造和解释》lisp 实现):
state_1$ = interval().pipe(debounceTime(300),startWith(4))
你实际上相当于写了:
1 | state_1$ = intervalVar |
你只需要对这个程序整体进行测试,而忽略中间运算符部分,还是那句话,没人会去测试 1+1=2
而如果整个程序都是这样的管道,你就只需要进行 e2e 测试,而不需要进行单元测试了(当然,自己实现的操作函数还是需要的),如果用校验替换掉 e2e ,动态提供反馈(某些重要领域的方案),那就不需要测试了,这就是—— 理想无 bug 系统
好了,说到这里,很多人会问,这个和 React 有什么关系?
因为 React Hooks,就是管道风格的极限函数式!
我们把 React Hooks 写法要求罗列一下:
- 不要在循环,条件或嵌套函数中调用 Hook
- 确保总是在你的 React 函数的最顶层调用 Hook
就在上面的例子中,我提到了,管道实际上相当于一段运算式,而运算式是不能顺序错乱的,如 1 + 1 = 2 不能写成 1 1 + = 2,波兰式就是波兰式,逆波兰式就是逆波兰式,你怎么设计的这个顺序,必须按照此种方式运行
举个例子:
1 | const [state,setState] = useState(0) |
你只要保证调用,他就能即刻被组装成:
1 | ReactScheduler$.pipe( |
这个按照次序调用的 hooks,就是管道操作函数,依赖数组就是管道函数形参,无形式参数是无法进行调用的(因为框架还要通过形参控制调度,除非框架帮你自动定位,不过个人觉得如果这个交给框架,问题比较严重,毕竟调度可是流中关乎生死存亡重要领域)
最后,纯粹的函数式是无法进行领域开发的,你尝试将那些管道自底向上组成一个应用?试试看?你就知道自底向上在应用架构上的无力
并且,副作用必须得到处理,纯粹函数式是没有副作用的,但是很抱歉,世界上没有纯粹的函数式(硬件 lambda 机已经被历史抛弃了),不可能每一个函数都是纯函数,这不切实际
所以,当你发现函数式在宏观尺度上比较乏力的时候,可以配合一起使用面向对象
极限的函数式,极限的面向对象可以共存,他们本身一个擅长异步和逻辑,一个擅长数据结构和业务建模
强强联合,你能写出体量巨大,性能卓越的应用(参考 Angular,领域驱动+响应式流)
但是 Redux 那个四不像嘛,包括所有主打所谓函数式的‘状态管理库’,该功成身退就功成身退,历史已经不需要你们了,走好不送~