[FE] 怎样按触发顺序执行异步任务

1. 异步任务

我从具体的项目中分离出了一个有趣的问题,可以描述如下:

页面上有一个按钮,每次点击它,都会发送一个ajax请求,
并且,用户可以在ajax返回之前点击它。

现在我们要实现一个功能,
以按钮的点击顺序展示ajax的响应结果。

2. 准备活动

为了以后编码的方便,先将ajax请求mock一下,

let count = 0;

// 模拟ajax请求,以随机时间返回一个比之前大一的自然数
const mockAjax = async () => {
    console.warn('send ajax');
    await new Promise((res, rej) => setTimeout(() => res(++count), Math.random() * 3000));
    console.warn('ajax return');
    return count;
};

然后,假设按钮的idsendAjax

<input id="sendAjax" type="button" value="Click" />

3. 冷静再冷静

document.querySelector('#sendAjax').addEventListener('click', async () => {
    const result = await mockAjax();
    console.log(result);
});

一开始,我们可能会想到这样的办法。
可惜,这是有问题的。

因为click事件,可能会在后面async函数还未返回之前,再次触发。
导致前一个请求还未返回,后面又发起了新请求。

其次,我们可能还会想到,记录每一个请求的时间戳,将结果排序
这也是有问题的,因为我们不知道未来还有多少次点击(<- 下文的关键信息),
如果无法拿到所有的结果,那么排序就有困难了。

那怎么办呢?
如果请求还未返回之前,能进行控制就好了。

4. 让我们Lazy一点

于是我想到了把新请求lazy化,放到一个队列中,
如果当前有其他任务在执行,就暂不处理。
否则,如果当前是空闲的,那就把队列中的任务都取出来,依次执行。

const PromiseExecutor = class {
    constructor() {
        // lazy promise队列
        this._queue = [];

        // 一个变量锁,如果当前有正在执行的lazy promise,就等待
        this._isBusy = false;
    }

    each(callback) {
        this._callback = callback;
    }

    // 通过isBusy实现加锁
    // 如果当前有任务正在执行,就返回,否则就按队列中任务的顺序来执行
    add(lazyPromise) {
        this._queue.push(lazyPromise);

        if (this._isBusy) {
            return;
        }

        this._isBusy = true;

        // execute是一个async函数,执行后立即返回,返回一个promise
        // 因此,add可以在execute内的promise resolved之前再次执行
        this.execute();
    };

    async execute() {

        // 按队列中的任务顺序来依次执行
        while (this._queue.length !== 0) {
            const head = this._queue.shift();
            const value = await head();
            this._callback && this._callback(value);
        }

        // 执行完之后,解锁
        this._isBusy = false;
    };
};

以上代码,我用了一个队列和变量锁,对新请求进行了管控。

其中的关键点是execute的异步性,
我们看到add函数在尾部调用了this.execute();,会立即返回。
这样就不会阻塞JavaScript线程,可以多次调用add函数了。

下面我们来看下它的使用方法吧,

const executor = new PromiseExecutor;

document.querySelector('#sendAjax').addEventListener('click', () => {

    // 添加一个lazy promise
    executor.add(() => mockAjax());
});

// 注册回调,该回调会按lazy promise的加入顺序,逐个获取它们resolved的值
executor.each(v => {
    console.log(v);
});

5. 更远一些

上文中有一句话,启发了我,
迫使我从不同的角度重新考虑了这个问题。

我们提到,由于“我们不知道未来还有多少次点击”,所以是无法进行排序的。
因此,我发现这是一个和“无穷流”相关的问题。
即,我们不应该把事件看成回调,而是应该看成流(stream)。

所以,我们可以寻找响应式的方式来解决它。
以下两篇文章可以帮你快速回顾一下响应式编程(Reactive Programming)。
——也称反应式编程 _(:зゝ∠)_

你所不知道的响应式编程
函数响应式流库探秘

好了,下面我们要开始进行响应式编程了。
首先,click事件可以形成一个“点击流”,

const clickStream = cont => document.querySelector('#sendAjax').addEventListener('click', cont);

这里的cont指的是Continuation,可以参考上面提到的第二篇文章。

其次,我们需要将这个“点击流”,变换成最终的“ajax结果流”,
并且保证“ajax结果流”的顺序,与“点击流”的顺序相同。

因此,问题在概念上就被简化了
事实上,所有的stream连同operator一起,构成了一个Monad

下面我们来编写operator吧,用来对流进行变换,我们只要记着,
什么时候调用cont就什么时候把东西放到结果流中”,即可。

const streamWait = function (mapValueToPromise) {
    const stream = this;

    // 使用一个队列和一个变量锁来进行调度
    // 如果当前正在处理,就入队,否则就一次性清空队列
    // 并且在清空的过程中,有了新的任务还可以入队
    const queue = [];
    let isBusy = false;

    return cont => stream(async v => {
        queue.push(() => mapValueToPromise(v));

        // 如果当前正在处理,就返回,不改变结果stream中的值
        if (isBusy) {
            return;
        }

        // 否则就按顺序处理,将每一个任务的返回值放到结果流中
        isBusy = true;
        while (queue.length !== 0) {
            const head = queue.shift();
            const r = await head();
            cont(r);
        }

        // 处理完了以后,恢复空闲状态
        isBusy = false;
    });
};

我们再来看下怎么使用它,是不是更加通俗易懂了呀。

// 点击流
const clickStream = cont => document.querySelector('#sendAjax').addEventListener('click', cont);

// ajax结果流
const responseStream = streamWait.call(clickStream, e => mockAjax());

// 订阅结果流
responseStream(v => console.log(v));

Your mouse is a database. —— Erik Meijer

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,475评论 4 372
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,744评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,101评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,732评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,141评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,049评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,188评论 2 320
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,965评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,716评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,867评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,341评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,663评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,376评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,200评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,990评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,179评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,979评论 2 279

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,111评论 18 139
  • 五十三:请解释 JavaScript 中 this 是如何工作的。1.方法调用模式当一个函数被保存为一个对象的属性...
    Arno_z阅读 525评论 0 2
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,598评论 25 707
  • 十几岁时的感情,回想起来,却格外动人。 那时候,还没有多大的压力,不用担心房租,不用担心由于加班赶不上末班车,不用...
    米恩阅读 117评论 0 0
  • 前几日读了艾明雅的一篇文《身体都不喜欢,心还凑合个屁咧》,很受启发。 女人是善妒的,由此女子之间...
    千誉嘉言阅读 442评论 4 2