序列化异步任务的三种典型问题

异步任务分为两大类:并行与串行。并行任务相对简单,串行任务则有多种变体。

Concurrency

并行的异步任务本质上是这样一个问题:单个异步任务仅可能返回两种状态:正常、异常。假设有 m(m > 1)个并发执行的异步任务合成一个异步任务集合,那么这个集合(也是一个异步任务)应该返回什么?

  • 全部正常才正常(只要有一个子任务异常,就返回异常)
    • 全部正常时,返回所有子任务结果的集合:Promise.all
    • 全部正常时,返回最先完成的子任务的结果:Promise.race
  • 存在正常就正常
    • 不管有没有异常,等所有子任务结束,返回所有结果的集合:Promise.settle
    • 不管有没有异常,只要出现一个子任务正常,就返回该任务的结果:Promise.any
  • 存在 n 个正常才正常,否则返回异常:Promise.some

Sequence

序列化异步问题是说:有些异步任务不能同时执行(互斥关系),必须等上一个执行完,才能执行下一个。来看几个典型场景:

1、下载队列问题

比如用户可以勾选任意多个文件并下载,假设我们的策略是下载完第一个再下载第二个,这种任务应该怎么实现呢?

可以借助某种队列或循环机制,比如通过 reduce 或 for of 来实现:

写法一:

写法二:

写法三:

市面上也有一些现成的库可以处理这种问题,比如 async.series、deferred-queue、promise-sequence 等。

2、loading 问题

假设每个接口请求发起时都会展示 loading,请求结束隐藏 loading。接口请求可能有很多,但每时每刻界面上只能有一个 loading。比如 a 请求发出,展示 loading,之后 b 请求发出,如果 a 请求结束时,b 还没有结束,那么继续展示 loading,反之则隐藏 loading,这怎么实现呢?

可以考虑一种引用计数的策略:

var loading = {
    count: 0,
    el: document.createTextNode('loading'),
    start () {
        if (this.count === 0) {
            document.body.appendChild(this.el)
        }
        this.count += 1
    },
    
    stop () {
        this.count -= 1
        if (this.count === 0) {
            document.body.removeChild(this.el)
        }
    }
}
复制代码

3、竞态问题

竞态问题是指同一类请求,先后发送,以哪一个的返回为准?比如用户搜索 A 类电影,由于接口迟迟未返回,用户选择搜索 B 类电影,如果 B 的请求还没有返回,A 却返回了,这时怎么办?

每个操作都只是单个异步任务,而不是一个序列任务,但用户的多次操作却构成了一个序列任务。

显然只有最新的请求才应该被使用,我们可以用时间戳来标识每个请求。

const map = {
    'fetchMovie': 0
}
function fetchMovie () {
    const stamp = Date.now()
    map.fetchMovie = stamp
    fetch(url, params).then(res => {
        if (stamp < map.fetchMovie) return null // 该请求已过时
        return res
    })
}
复制代码

不过这种方案侵入性比较强,能不能实现一个类似 redux-saga 中的 takeLatest 的方法呢?takeLatest 的基本思路是:只要有最新的请求,就将之前的请求 cancel 掉,但 promise 没有办法 cancel(I know bluebird),这怎么办呢?

// 模拟一个在 t 时间后返回结果的接口请求
function request (t) {
    return new Promise(resolve => {
        setTimeout(function () {
            resolve(t)
        }, t)
    })
}

const map = {}

function takeLatest (key, fn) {
    if (!map[key]) {
        map[key] = 1
    }

    return function () {
        let resolve
        let reject
        
        // 嘿嘿
        const a = new Promise((_res, _rej) => {
            resolve = _res
            reject = _rej
        })
    
        const t = Date.now()
        map[key] = t

        fn.apply(null, arguments).then(res => {
            if (t < map[key]) return
            resolve(res)
        }).catch(error => {
            if (t < map[key]) return
            reject(error)
        })
        
        return a
    }
}

// 测试
const f1 = takeLatest('fetchMovie', request)
const f2 = takeLatest('fetchOther', request)

f1(3000).then(res => {
    console.log(res)
})

f2(3050).then(res => {
    console.log(res)
})

setTimeout(() => {
    f1(1000).then(res => {
        console.log(res)
    })
    
    f2(1050).then(res => {
        console.log(res)
    })
}, 1000)

// 返回 setTimeout 里的两个“最新”的请求结果:1000,1050
复制代码

Webpack 的异步任务管理

webpack 原本只是一个打包工具,后来逐步演化成前端构建工具,构建的核心是构建过程管理,或者说构建任务管理。任务有同步、有异步,过程有并行、有串行,看上去很复杂,但是 webpack 通过 tapable 这个库对各类同步、异步场景做了很好的抽象,tapable 提供了一系列被称为 hook 的 api 来处理各类同步、异步的场景。

{
    AsyncParallelHook,// 异步并行任务
    AsyncSeriesHook, // 异步序列任务
    AsyncSeriesBailHook, // 可中断的异步序列任务
    AsyncSeriesWaterfallHook // 可传参的异步序列任务
    ...
}
复制代码

这篇文章 讲得很清楚,推荐。

你可能感兴趣的:(序列化异步任务的三种典型问题)