Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

怎样按触发顺序执行异步任务 #27

Open
thzt opened this issue Aug 30, 2018 · 6 comments
Open

怎样按触发顺序执行异步任务 #27

thzt opened this issue Aug 30, 2018 · 6 comments
Labels
async javascript es5、es6、babel 精选系列:反应式编程 小编推荐反应式编程系列

Comments

@thzt
Copy link

thzt commented Aug 30, 2018

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

@acodercc
Copy link
Member

嗯,不错的异步问题,要看具体业务的要求,有时候是需要每次点击都要执行异步操作。

有时候业务要求,必须等待上一次操作完成后,才能执行下一次操作。

@xc1427
Copy link

xc1427 commented Aug 30, 2018

异步问题是核心问题

@acodercc
Copy link
Member

图文并茂很棒 👍

@shaozj
Copy link

shaozj commented Aug 30, 2018

利用队列和变量锁来使得每个异步任务依次执行,很赞的思路。其实根据需求,如果只是需要按点击顺序展示 ,那么从性能上考虑,并发请求,结果排序是性能更好的。但是如果各个异步操作之间存在依赖关系,下个异步操作依赖于上个异步操作。那么这个方法真的非常好。

@fongfai
Copy link

fongfai commented Nov 26, 2023

利用队列和变量锁来使得每个异步任务依次执行,很赞的思路。其实根据需求,如果只是需要按点击顺序展示 ,那么从性能上考虑,并发请求,结果排序是性能更好的。但是如果各个异步操作之间存在依赖关系,下个异步操作依赖于上个异步操作。那么这个方法真的非常好。

并发请求的话excue那里稍微改造一下就能用,

 while(this.queues.length){
      // single request
      const task = this.queues.shift()
      const result = await task()
      this.results.push(result)
      this._callback && this._callback(value);
      // concurrent request
      const batchTasks = this.queues.splice(0, this.REQUEST_SIZE)
      const results = await Promise.all(batchTasks.map(t => t()))
      this.results.push(result)

    }
`

@xpl028
Copy link

xpl028 commented Nov 26, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
async javascript es5、es6、babel 精选系列:反应式编程 小编推荐反应式编程系列
Projects
None yet
Development

No branches or pull requests

7 participants