转载自前端备忘录-江湖术士

基础函数式,比如纯函数,柯里化什么的,就先不讨论了,大家可以自行搜索,这部分文章非常多

如果形成一个标准封装的函数结构,那么我们可以做些什么事情呢?

1
2
3
4
5
6
7
8
9
function someFunc(state) {
// just mutate state
state.xxx = xxx;
return state;
}
function anotherFunc(state) {
state.otherXxx = otherXxx;
return state;
}

当我们做了科里化之后,两个函数如果共享同一个参数和返回数据,我们可以把它们连接起来

someFunc(antherFunc(state))

这就构成了一个流水生产线,以此类推还可以叠加更多

someFunc1(someFunc2(someFunc3(someFunc4)))

通过类似 ramda 库中的 compose 封装一下,可以更加美观:

R.compose(someFunc3,someFunc2,someFunc1)()

注意顺序

这就很明显是个管道了,并且是个单向数据流:

$.pipe(someFunc3,someFunc3,someFunc1)

好了,到这里,熟悉过相关工具的人就会发现,这就是 cycle.js 或者 rxjs 的标准结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// cycle.js
const state$ = props$
.map((props) =>
newValue$
.map((val) => ({
label: props.label,
unit: props.unit,
min: props.min,
value: val,
max: props.max,
}))
.startWith(props)
)
.flatten()
.remember();

其一: 函数式相关优势

由于 state 是统一的,并且数据流中前后函数的不变形和纯度不需要过多的人为控制,并不像类似 redux 那样,还需要你手动保持函数纯度(如果挑选适合的操作函数的话,map,tap 先不讨论)

其二:类型推断

管道函数的类型不需要你自己来手动声明,他会依次传递,到了你所在的节点,形参必然会有类型

其三:测试友好

操作函数是最小功能单位,符合函数式自底向上开发模式,不需要进行单元测试 —— 同一个单位,如果没有副作用,一但出了错误,那肯定是逻辑的问题,因为它只有入参和出参,逻辑又是确定的,稳定性又由第三方(比如微软)保证,当前开发层级中,对于连接起来的单向数据流,没有测试的必要,只需要验证功能

举个例子,debounce,startWith 是 Rxjs 操作函数,它的稳定性有微软和部分第三方开发者保证,在使用途中将其视为运算符(你会测试 1+1=2 么?),于是使用时,你相当于自己利用他们发明了一个新语言(《计算机程序的构造和解释》lisp 实现):

state_1$ = interval().pipe(debounceTime(300),startWith(4))

你实际上相当于写了:

1
2
3
state_1$ = intervalVar
state_1$ debounceTime 300
state_1$ startWIth 4

你只需要对这个程序整体进行测试,而忽略中间运算符部分,还是那句话,没人会去测试 1+1=2

而如果整个程序都是这样的管道,你就只需要进行 e2e 测试,而不需要进行单元测试了(当然,自己实现的操作函数还是需要的),如果用校验替换掉 e2e ,动态提供反馈(某些重要领域的方案),那就不需要测试了,这就是—— 理想无 bug 系统

好了,说到这里,很多人会问,这个和 React 有什么关系?

因为 React Hooks,就是管道风格的极限函数式!

我们把 React Hooks 写法要求罗列一下:

- 不要在循环,条件或嵌套函数中调用 Hook
- 确保总是在你的 React 函数的最顶层调用 Hook

就在上面的例子中,我提到了,管道实际上相当于一段运算式,而运算式是不能顺序错乱的,如 1 + 1 = 2 不能写成 1 1 + = 2,波兰式就是波兰式,逆波兰式就是逆波兰式,你怎么设计的这个顺序,必须按照此种方式运行

举个例子:

1
2
3
4
5
6
7
const [state,setState] = useState(0)
useEffect(()=>{
console.log(map 1 time)
},[state])
useEffect(()=>{
console.log(map 2 time)
},[state])

你只要保证调用,他就能即刻被组装成:

1
2
3
4
5
ReactScheduler$.pipe(
startWith({state:0,setState(res){this.state = res}}),
map((res)=>{console.log(map 1 time);return res}),
map((res)=>{console.log(map 2 time);return res}),
)

这个按照次序调用的 hooks,就是管道操作函数,依赖数组就是管道函数形参,无形式参数是无法进行调用的(因为框架还要通过形参控制调度,除非框架帮你自动定位,不过个人觉得如果这个交给框架,问题比较严重,毕竟调度可是流中关乎生死存亡重要领域)

最后,纯粹的函数式是无法进行领域开发的,你尝试将那些管道自底向上组成一个应用?试试看?你就知道自底向上在应用架构上的无力

并且,副作用必须得到处理,纯粹函数式是没有副作用的,但是很抱歉,世界上没有纯粹的函数式(硬件 lambda 机已经被历史抛弃了),不可能每一个函数都是纯函数,这不切实际

所以,当你发现函数式在宏观尺度上比较乏力的时候,可以配合一起使用面向对象

极限的函数式,极限的面向对象可以共存,他们本身一个擅长异步和逻辑,一个擅长数据结构和业务建模

强强联合,你能写出体量巨大,性能卓越的应用(参考 Angular,领域驱动+响应式流)

但是 Redux 那个四不像嘛,包括所有主打所谓函数式的‘状态管理库’,该功成身退就功成身退,历史已经不需要你们了,走好不送~