赞
踩
(知乎上的动图压得太厉害了 ,可以在语雀上查看高清版)
异步逻辑问题一直是前端开发中的难点之一。我们知道 JavaScript 是基于事件循环机制执行的,代码并非以视觉顺序从上到下运行,而是取决于相关事件发生的时间与次序。随着前端页面的复杂度不断上升,代码中需要处理的事件类型越来越多:用户交互(键盘/鼠标)、网络请求与响应、服务器消息推送、组件生命周期回调……。当不同的事件交织在一起时,维护复杂异步逻辑的成本是非常高的。此时我们应当借助一些合适的工具以降低书写异步代码的复杂度,例如使用 Promise 优化回调函数的写法,利用 async/await 简化 Promise 的创建与使用,抑或是引入 redux-saga/RxJS 这样的类库来针对性地解决问题。
以下面的表格为例,考虑表格需要展示大量数据,前端采用了异步加载数据的方式。用户每点击一次展开按钮,表格会从后端去加载对应节点下方的内容。用户点击某个子公司之后,表格会发起请求查询该子公司下有哪些门店。
在实际使用中,用户可能会连续展开多个节点,表格将连续发送多个请求。在理想情况下,后端处理速度足够快,后端响应总是快于下一次用户交互,我们在开发时只需要考虑“展开1——响应1——展开2——响应2”的事件顺序,代码写起来也较为简单。但在请求处理较慢时,例外情况就会发生:上一次响应尚未回来时,用户进行了点击操作(展开1——展开2——响应1——响应2);或是响应顺序与请求顺序不一致(展开1——展开2——响应2——响应1);也可能是因为响应太慢,在响应回来之前,用户切换了页面(前端路由),表格组件被卸载了。如果我们在开发时不注意这些细节,那么难免会导致以下问题:
一般来说,对于「上一个响应尚未回来,下一个请求将被发起」的情况,我们可以有以下几种处理方案(以下简称异步处理方案):
上述每一种异步处理方案都有各自适用的场景,在编程中我们需要分析实际情况来为每一种交互挑选最为合适的方案。
上面的各个方案只是在模型/逻辑层面对请求进行了处理,保证了页面在极端情况下页面的鲁棒性;为了使得页面更加易用,在视图层面我们也需要透出相应的载入/出错提示,让用户直观感受到正在进行的操作与操作的结果。
在 RxJS 的世界中,有许多操作符能够将高阶的 Observable 转换为一阶的 Observable,例如 mergeAll, switchAll, exhaust… 这些操作符会以不同的方式来「打平」高阶 Observable,这些不同方式恰好对应了前面的各个异步处理方案(这也是方案名称的由来)。从这里我们也可以看出 RxJS 强大的表达能力:当我们用 Observable 去表达表格状态、表格事件,并用操作符的组合去实现表格逻辑之后,通过切换操作符就可以切换表格采取的异步处理方案。
在其他实现方式下,实现单个方案就有不小的成本(例如通过回调函数去实现 concatAll 方案,需要手动维护一个队列),更不用提在不同方案之间进行切换的成本了。而在 RxJS 下,异步方案与具体表格数据加载逻辑是完全解耦的,切换方案只需要简单地切换操作符即可。而当需要新增一个自定义方案时,我们只需要实现一个新的操作符即可。
下面附上相关代码和不同方案下的表格行为:
- const loadLeftOrLoadTop$ = action$.pipe(
- filter(action => action.type === 'expand-left' || action.type === 'expand-top'),
- map(action =>
- of(action).pipe(
- withLatestFrom(dataAdapter$, baseQueryConfig$),
- // of(action) 只会同步地发送一个值然后立刻结束,所以这里可以用任意 join 操作符
- switchMap(([action, dataAdapter, baseQueryConfig]) => {
- const promise = dataAdapter.queryDrillTree(baseQueryConfig, action.node)
- return from(promise).pipe(
- // 得到响应的时候重新获取一遍最新的 state$;因为在响应回来之前 state$ 可能已经发生变化
- withLatestFrom(state$),
- map(([subTree, state]) => {
- if (action.type === 'expand-left') {
- return {
- type: 'load-left',
- leftTree: replaceSubTree(state.leftTree, subTree),
- }
- } // else 表格上方逻辑与左侧逻辑相同,这里省略
- }),
- )
- }),
- ),
- ),
- // 这里用 concatAll 来将所有的 expandLeft/expandTop 操作放入到一个队列,保证每个操作依次执行
- concatAll(),
- // 我们可以替换成其他操作符(switchAll/exhaust...)来切换异步处理方案
- // map(...), concatAll(...) 也可以优化为 concatMap(...)
- )
concatAll 可以参见本文第一张动图。debounce/buffer 方案与高阶操作符方案类似,不过该方案还需要另一个 Observable 作为抖动结束的信号或是 flush buffer 的信号,实现起来与前述方案差别不大,这里就不再展开了。
值得一提的是,在 redux-saga 中,我们也可以通过切换 takeLatest/takeLeading/takeEvery 等 saga helpers 声明式地切换不同异步处理方案。不过因为 redux-saga 要以 redux middleware 的形式运行,对环境依赖较高;而 RxJS 则相对地可以跑在更多地方,本文中的例子就是将 RxJS 跑在了 React Hooks 中(我拿 LeetCode-OpenSource/rxjs-hooks 稍微改了一下)。
从上文中可以看出在前端开发中异步问题并不简单,即便对于「上一个响应尚未回来,下一个请求将被发起」这样一个问题,我们也要分析实际情况,并从多个方案中挑选一个最合理的方案。同时这些方案与 RxJS 中的一些高阶操作符是不谋而合的,当我们将逻辑、状态、事件等用 Observable/operators 抽象出来时,我们便能充分享受 RxJS 带来的好处,RxJS 能够让我们站在更高的角度去抽象和复用代码。最后祝大家 happy coding
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。