阅读笔记:传统上,我们会将功能作为区分应用类别的维度。比如:管理后台、活动 H5、聊天 IM、电商购物、视频直播……我们有非常多细分领域,每个领域都有不同的业务痛点和侧重点
复杂的前端应用简单地分为两类:数据驱动和事件驱动。
数据驱动:业务复杂度完全来自于后台无穷无尽的数据和复杂业务流程。比如12306复杂的后端逻辑。虽然前端只需要提交一个表单即可。
事件驱动:复杂度来自于 用户的输入事件。
一个富文本编辑器在编辑时就算完全不对接后台接口,光是处理用户的粘贴、选中和键盘等事件,就可以成为传说中的『天坑』
构建这类应用的时候,其难点主要来自于在大量不同类型的异步事件可以任意地排列组合,使得可能的状态空间极度膨胀而容易出错——相信只要在页面中同时维护过几个定时器的同学都能理解。
redux拥有时间旅行的能力,我们的应用在遇到类似需要回溯状态的场景时,上 Redux 的风险更小
事件驱动的前端应用,非常重度依赖时间旅行类的技术。
市面上几乎所有的靠谱富文本编辑器,都维护了自己的一套撤销栈——这就是时间旅行的核心功能!对这类应用,时间旅行甚至是影响体验的核心因素之一。
甚至,只要撤销功能实现得好,用户在遇上预期外行为乃至编辑器 bug 的时候,也能自己撤销回去,然后尝试其它的交互方式来达成目标——时间旅行是用户体验最后的守卫者!
对于一个富文本编辑器来说,如果想要表达『表格里支持嵌套表格』的信息,Redux 对应的原生 JSON 数据结构也显得非常单薄,基本必须上 Immutable。(源码:Slate.js里直接使用immutable跳过了redux)
全局状态的内存模型不符合经典的计算机体系结构。对于一个比浏览器中网页复杂得多的桌面 GUI,每个窗口对应的进程,其对应的内存空间是相互独立,还是混杂在一个支持时间旅行的『全局状态』里呢?——这不正说明了桌面操作系统的落后吗!Mac 和 Windows 这些老古董能像我们基于 Redux 写的网页这样优雅地时间旅行吗?
Redux 确实解决了一个痛点问题,即深度嵌套的组件间状态通信的问题。但解决这个问题,并不代表着我们就必须把状态全部提到全局层面。这个问题的体现,可以简单理解为: 在 A 组件里实现的方法,触发它的事件在 B 组件里,而 C 组件又需要订阅执行结果…… 这时候纯 React 处理起来确实棘手,但只要将 store 放置在 A、B、C 三个组件中最顶级的一个里——而不需要放置在全局——而后通过 Context 的定制,就足够解决这个问题了。
事件驱动型的前端应用,时间旅行很重要但是实现并不是引入一个redux架构就可以解决了,比如当实现一个富文本的时候,列举几个业务中遇到的具体例子:
- 在使用 Slate.js 时,撤销栈在某些情况下会被意外清空。阅读了源码后我们发现,当时的撤销栈实现,会把编辑器初始化时的更改作为栈的第一项推进去。在尝试撤销掉这一项的时候,带来的副作用会意外地破坏编辑器的计数逻辑,导致本应可以重做回去的内容丢失。这个 bug 我们已经提 PR 解决了,但类似的撤销栈细节 Issue 还有不少。
- 一些业务场景,在撤销与重做时很难通过 push 和 pop 这样基本的栈操作解决。譬如,在上传图片的过程中用户仍然可以输入文本,这时对『进度条进度变更』的撤销事件操作,就会在撤销栈中和用户的输入事件相互『夹杂』而加大撤销的难度。
- 对连续发生的输入事件,需要做不同的去重处理。比如用户连续地输入了一行文本,那么在撤销时,就需要一次性将整行撤销;而如果用户缓慢地逐字输入,那么就应该逐字撤销。
对于一些复杂度更高的场景(如富文本编辑的实时协同),这时实现时间旅行的基础就已经不再是简单的撤销栈 + 全量状态替换,而是已经涉及到 OT 等足够写不少论文的高级算法了
rxjs与事件驱动应用:(响应式编程)
事件驱动的前端应用中,对异步逻辑的把握则显得非常重要。Rx 的事件流思维模型