Redux 学习起来很困难?写起代码来很啰嗦?
一起来看看 Rematch 的作者对 Redux 的思考和简化吧~
原文:《Redesigning Redux》, Shawn McKay
都过了这么多年了,状态管理的问题难道不应该早就被解决了么?
个人直觉,开发者似乎都承认一个潜规则,那就是状态管理问题似乎比想象的更复杂。在本文,我们将一起探讨你可能已经遇到的问题:
- 我们真的需要一个状态管理库么?
- Redux 的流行是否实至名归?为什么?
- 我们是否能够提出更好的状态管理方案?如果是,怎么做?
真的需要状态管理库?
前端开发并不仅仅是左右移动像素点。开发的真正艺术是掌握如何管理状态。
简单粗暴的答案是:状态管理是复杂的,但是并非那么复杂。
以基于组件的视图框架/库为例,比如 React,让我们来看看我们有哪些选择:
1. 组件状态
存在于单个组件内部的状态。在 React 中,就是指使用 setState
来更新的 state
。
2. 相关状态
父组件传递给子组件的状态。在 React 中,就是指通过属性传递给子组件的 props
。
3. 供给状态
保存在根提供者中、通过组件树传递给消费者的状态。在 React 中,对应于 context API
。
大多数的状态都是存在于视图中的,因为它是用来反映用户界面的。那么,对于反映底层数据和逻辑的其它状态,又属于谁呢?
如果把所有的状态都填塞到视图中,那么就严重违背了关注点分离原则。它会把你牢牢地绑在一个 JS 视图库中,使得代码难以测试。更有甚者,可能会为你带来大麻烦,因为你必须持续不断的思考和调整状态的存储之处。
由于架构设计的改变,状态管理会随之变得复杂,并且通常很难判断哪些组件需要哪些状态。最简单的做法是,在根组件上包含所有状态。如果真要这么做的话,那么选用下一种方式会更好。
4. 外部状态
状态是可以从视图库中移出来的,然后可以使用提供者/消费者模式把状态重新连接回视图库。
Redux 也许是最流行的状态管理库。它在过去的 2 年时间里取得了巨大的流行度。那么,为什么一个简单的库会获得如此之多的关注呢?
是因为它的性能更高么?其实并不是。实际上,它反而让每一个必须处理的回调变得更慢了些。
那是因为它更简单?也决定不是。
论简单的话,那么纯 JS 才是。正如 TJ 所说:
那为什么大家不直接使用 global.state = {}
呢?
为什么使用 Redux
本质上,Redux 跟 TJ 所说的是同一件事,只不过 Redux 封装了一些管道工具而已。
在 Redux 中,我们不能直接修改状态。修改状态的唯一方式是分发(Dispatch)一个动作(Action)到管道中,管道会自动根据动作去更新状态。
从上图可以看到,管道的中有 2 个监听器集合:中间件(Middleware)和订阅(Subscription)。中间件一些是监听动作的函数,用来处理类似日志记录、开发工具和发起服务请求等功能。订阅则是一些用来广播状态变更的函数。
最后,合成器(Reducer)函数负责把状态变更拆分成更小、更模块化、更容易管理的代码块。
和使用一个全局对象相比,Redux 确实简化了开发过程。
综上,我们可以把 Redux 看成是一个全局对象,该对象不仅提供了状态更新前/后钩子,而且能够以简单的方式合成新状态。
不觉得 Redux 过于复杂么?
的确过于复杂。在平时的开发过程中,有一些不可否认的迹象,可以用来判断框架 API 是否需要改进。这些迹象可以归纳为下面这个公式:
其中,节省的时间是指使用该框架来开发所消耗的时间,学习的时间则是阅读框架文档、学习教程和掌握新概念的总时间。
Redux 本身是个精简的库,但是其学习曲线却很陡峭。虽然有不少开发者能够克服深入学习函数式编程的困难并从 Redux 获益良多,但是也有很多开发者望而却步,宁愿重新使用 jQuery。
在 jQuery 中,你并不需要理解什么是 “comonad”,也没必要理解通过函数组合来管理状态。
任何框架或者库的目的都应该是把复杂的事物抽象得更加简单。
当然我这么说并不是想指责 Redux 的作者 Dan Abramov 。Redux 在其刚诞生初期就已经变得非常流行,没有足够的时间来精雕细琢。
- 我们怎么能随便重构一个已经被成千上万开发者使用的库呢?
- 我们又怎么能合理发布会影响到不计其数项目的重大变更呢?
没人可以。但是我们可以提供更好的支持,比如完善文档、推广视频教程和扩大社区等。在这些方面,Dan Abramov 确实做得很棒。
又或者,还有另一种方式可以改变现状。
重新设计 Redux
在我看来,重写 Redux 是有其必要性的,至少有以下 6 个方面可以改进得更友好。
1. 初始化过程
让我们来看看一个基本的 Redux 初始化过程,如下图左边所示:
很多开发者走到这里一步就开始止步了,简直是一看三不知。什么是 chunk ?compose 又是什么鬼?一个函数还能这么使用?
如果 Redux 是基于配置而不是函数组合的话,那么像右边那样的初始化过程明显看起来更加合理。
2. 简化状态合成器
Redux 中的状态合成器能够使用一个 switch
来代替多个不必要的 switch
。
假如状态合成器是根据动作的类型来匹配的,那么我们可以用逆向思维,把合成器变成一个接受 state
和 action
两个参数的纯函数。也许还可以更简单些,我们可以把动作规划化,并且只传递状态和一个数据载荷。
3. 使用 Async/Await 代替 Thunks
在 Redux 中,Thunks最通用的做法就是用来创建异步动作。从多角度来看,这种用法更像是一个聪明的黑客所采用的用法,而不是一种官方推荐的用法。我们一步一步来看:
- 首先分发了一个动作,然而实际上却是一个函数而不是期望的对象
- Thunk 中间件检查每一个动作,看它是否是一个函数
- 如果是函数,那么中间件调用该函数,同时把
dispatch
和getState
方法传参进去
真的需要这样么?把一个简单的动作在运行时识别为对象、函数甚至是 Promise,这难道不是糟糕的实践么?
如上图右边所示,难道我们就不能只使用 async/await ?
4. 两种类型的动作
如果我们认真想想的话,确实有两种类型的动作:
- 合成器动作(Reducer action): 触发合成器然后改变状态
- 副作用动作(Effect action):触发一个异步动作。这可能也称为合成器动作,但是异步函数其实并没有直接改变任何状态。
因此,把这两种类型的动作区分开来会更有用,同时也不容易与 Thunks 搞混。
5. 不再需要定义动作类型变量
为什么我们的标准实践要把动作生成器和状态合成器区分开来呢?能否只用其中一个呢?改变其中一个又是否会影响到另一个?
在我看来,动作生成器和状态合成器就是硬币的两个面。
const ACTION_ONE = 'ACTION_ONE'
可以说是把动作生成器和状态合成器分开的冗余产物。如果我们把这两者合二为一,那么就不会有那么多文件专门导出这些类型字符串了。
6. 合二为一的合成器
按照使用方式,把 Redux 中所涉及的概念进行合并分组,那么我们可以得出下面这个更简单的模式。
在这种模式中,状态合成器是可以自动确定与之对应的动作生成器。因为,此时状态合成器可以自动变成动作生成器。
通过简单的命名约定,以下行为都会变得可预测:
- 如果状态合成器命名为
increment
,那么动作类型就是increment
。更好的做法是加上命名空间:count/increment
。 - 每个动作都通过
payload
属性来传递数据。
这样的话,对于 count.increment
,我们就可以自动的从状态合成器推导出动作生成器。
好消息:更棒的 Redux
以上的通电就是我们创建 Rematch 的原因。
Rematch 对 Redux 进行了封装,提供更简单的 API,但又不失任何可配置性的特点。
下面是一个完整的使用例子:
我已经把 Rematch 应用在生成环境中有好几个月了。作为小白鼠,我的体验是:
状态管理从未变得如此简单、高效。
Redux 并没有被抛弃,而且也不应该被抛弃。
只是,我们应该以更低的学习成本,更少的样板代码和更少的认知成本,来拥抱 Redux 背后的简单哲学。
赶紧试一试 Rematch 吧!
万一一不小心你就爱上它了呢?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。