JS 中,串行异步任务的取消是否有更好的处理方式

159 天前
 dvsilch

最近遇到的业务场景:

  1. 用户输入时进行初始化,初始化过程中包含了异步任务的串行
  2. 初始化过程中,不阻塞用户输入

如果用户输入时上一次的初始化还没结束,就会出现多次初始化的并行。我目前的处理方式是初始化开始时记下当前状态,每结束一个串行的异步任务,都进行一次状态比对。简化后的代码如下:

/**
 * @type {number | undefined}
 */
let state = undefined

/**
 * @param {number} ms
 * @returns {Promise<void>}
 * @description sleep for ms milliseconds to simulate async task
 */
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

/**
 * @returns {Promise<void>}
 * @description init async task
 */
async function init() {
  const current = Date.now()
  state = current

  await sleep(500)
  if (state !== current) {
    console.warn(`state ${current} init: canceled`)
    return
  }

  await sleep(500)
  if (state !== current) {
    console.warn(`state ${current} init: canceled`)
    return
  }

  console.log(`state ${current} init: done`)
  state = undefined
}

但我感觉这种做法不太合理,虽然上面用当前时间戳模拟了状态,但实际上业务场景中可能会出现某几次用户输入一致,所以状态一致,进而导致这几次输入执行的初始化过程无法正常中断。像 C#中有 CancellationToken 可以直接调用 Cancel 取消异步任务。不知道 JS 是否拥有类似的设计,或者对于这类业务场景有更好的处理方式?

3057 次点击
所在节点    JavaScript
33 条回复
dvsilch
158 天前
@jones2000 是的,其实跟上面使用 generator 的思路一致
edward1987
158 天前
也可以简单的写个装饰函数,把所有异步任务的函数都装饰一遍再调用
function trans(funcN){
return function(...args){
if(state!==current){return};
return funcN(...args)
}
}
然后重新赋值每个异步任务函数
funcA = trans(funcA);
funcB= trans(funcB);
yigefanqie
157 天前
没太理解业务场景,这不应该是给用户输入的事件加 debounce 的事吗
nzbin
157 天前
只要是异步,毫无疑问 rxjs 就是更好的处理方式
ddch1997
157 天前
我是用 redux-saga ,fork 一个异步 task 任务,然后 take 一个停止 action ,cancel 掉这个 task 任务就好
cjd6568358
156 天前
把所有的串行异步用 promise.then 链式连接起来。对外暴露 promise,每次用户有新输入调用 promise.reject 终止异步任务链。然后生成新的
RabbitDR
156 天前
@yigefanqie 不一样,他是想取消并行的初始化,防抖不会取消函数的执行。比如连续输入 abc 后 abc 三次输入被防抖了,开始初始化,但这时用户再输入 d ,会进行第二次初始化,他想第二次初始化时如果第一次还在初始化则中止第一次的初始化。
yigefanqie
156 天前
我理解的这样:用户输入时的异步串行任务是不是都是无副作用的,如果是无作用的,直接丢弃之前的初始化,重新初始化就行了,配合上 debounce 降低下初始化触发频率。丢弃掉的初始化如果都是 promise 不用担心内存问题,会自动回收的。
yigefanqie
156 天前
@RabbitDR 不太会回复。回复发在楼上一层了。
tsanie
155 天前
@yigefanqie #28 一般初始化都是资源占用型过程,最佳实践还是直接中断。

不过对 op 提到的 CancellationToken 有些疑问,token 自身是无法 Cancel 的,只能通过 IsCancellationRequested 获取终止状态或者调用 ThrowIfCancellationRequested() 在被终止时抛出异常,和 js 中的 AbortSignal 是一样样的( aborted, throwIfAborted())

可以主动调用 Cancel 的是 CancellationTokenSource ,对应在 js 中是 AbortController
dvsilch
155 天前
@tsanie
是我笔误了,确实是 CancellationTokenSource(cts),C#里可以做到外部内部各提供一个 cts ,初始化时将两个 cts link 然后将 ct 链式传递到各个异步任务,需要中断时任意一个 cts 直接 cancel 即可,会立刻走到 catch OperationCanceledException 的分支

但我看了一下 AbortController 似乎只能做到对 fetch api 执行中断,不是特别符合我当前的业务场景...不仅仅是网络请求,还有一些文件的读取、以及等待用户另外输入的 Promise 。这部分如果取消不掉的话,我能想到的形式就是要么把各个 reject 动态推入移出队列,要么就是每一轮异步任务结束后判断状态。两种做法都感觉不太合理,所以目前的做法是暂时跟 #9 #15 一样写一个自定义的 generator 来统一这部分逻辑,有空再去研究研究 rxjs 的实现
zy445566
155 天前
嘿嘿 这里还挺有讨论意义 自己也写了一个暂停取消任务给自己使用

https://github.com/zy445566/pause-task-line
forty
125 天前
用单独的线程执行任务,需要取消的时候直接杀线程,简单粗暴又有效

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1057003

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX