[FE] 怎樣按觸發(fā)順序執(zhí)行異步任務(wù)

1. 異步任務(wù)

我從具體的項(xiàng)目中分離出了一個(gè)有趣的問題,可以描述如下:

頁面上有一個(gè)按鈕,每次點(diǎn)擊它,都會(huì)發(fā)送一個(gè)ajax請(qǐng)求,
并且,用戶可以在ajax返回之前點(diǎn)擊它。

現(xiàn)在我們要實(shí)現(xiàn)一個(gè)功能,
以按鈕的點(diǎn)擊順序展示ajax的響應(yīng)結(jié)果。

2. 準(zhǔn)備活動(dòng)

為了以后編碼的方便,先將ajax請(qǐng)求mock一下,

let count = 0;

// 模擬ajax請(qǐng)求,以隨機(jī)時(shí)間返回一個(gè)比之前大一的自然數(shù)
const mockAjax = async () => {
    console.warn('send ajax');
    await new Promise((res, rej) => setTimeout(() => res(++count), Math.random() * 3000));
    console.warn('ajax return');
    return count;
};

然后,假設(shè)按鈕的idsendAjax,

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

3. 冷靜再冷靜

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

一開始,我們可能會(huì)想到這樣的辦法。
可惜,這是有問題的。

因?yàn)?code>click事件,可能會(huì)在后面async函數(shù)還未返回之前,再次觸發(fā)。
導(dǎo)致前一個(gè)請(qǐng)求還未返回,后面又發(fā)起了新請(qǐng)求。

其次,我們可能還會(huì)想到,記錄每一個(gè)請(qǐng)求的時(shí)間戳,將結(jié)果排序,
這也是有問題的,因?yàn)?strong>我們不知道未來還有多少次點(diǎn)擊(<- 下文的關(guān)鍵信息),
如果無法拿到所有的結(jié)果,那么排序就有困難了。

那怎么辦呢?
如果請(qǐng)求還未返回之前,能進(jìn)行控制就好了。

4. 讓我們Lazy一點(diǎn)

于是我想到了把新請(qǐng)求lazy化,放到一個(gè)隊(duì)列中,
如果當(dāng)前有其他任務(wù)在執(zhí)行,就暫不處理。
否則,如果當(dāng)前是空閑的,那就把隊(duì)列中的任務(wù)都取出來,依次執(zhí)行。

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

        // 一個(gè)變量鎖,如果當(dāng)前有正在執(zhí)行的lazy promise,就等待
        this._isBusy = false;
    }

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

    // 通過isBusy實(shí)現(xiàn)加鎖
    // 如果當(dāng)前有任務(wù)正在執(zhí)行,就返回,否則就按隊(duì)列中任務(wù)的順序來執(zhí)行
    add(lazyPromise) {
        this._queue.push(lazyPromise);

        if (this._isBusy) {
            return;
        }

        this._isBusy = true;

        // execute是一個(gè)async函數(shù),執(zhí)行后立即返回,返回一個(gè)promise
        // 因此,add可以在execute內(nèi)的promise resolved之前再次執(zhí)行
        this.execute();
    };

    async execute() {

        // 按隊(duì)列中的任務(wù)順序來依次執(zhí)行
        while (this._queue.length !== 0) {
            const head = this._queue.shift();
            const value = await head();
            this._callback && this._callback(value);
        }

        // 執(zhí)行完之后,解鎖
        this._isBusy = false;
    };
};

以上代碼,我用了一個(gè)隊(duì)列和變量鎖,對(duì)新請(qǐng)求進(jìn)行了管控。

其中的關(guān)鍵點(diǎn)是execute的異步性,
我們看到add函數(shù)在尾部調(diào)用了this.execute();,會(huì)立即返回。
這樣就不會(huì)阻塞JavaScript線程,可以多次調(diào)用add函數(shù)了。

下面我們來看下它的使用方法吧,

const executor = new PromiseExecutor;

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

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

// 注冊(cè)回調(diào),該回調(diào)會(huì)按lazy promise的加入順序,逐個(gè)獲取它們r(jià)esolved的值
executor.each(v => {
    console.log(v);
});

5. 更遠(yuǎn)一些

上文中有一句話,啟發(fā)了我,
迫使我從不同的角度重新考慮了這個(gè)問題。

我們提到,由于“我們不知道未來還有多少次點(diǎn)擊”,所以是無法進(jìn)行排序的。
因此,我發(fā)現(xiàn)這是一個(gè)和“無窮流”相關(guān)的問題。
即,我們不應(yīng)該把事件看成回調(diào),而是應(yīng)該看成流(stream)。

所以,我們可以尋找響應(yīng)式的方式來解決它。
以下兩篇文章可以幫你快速回顧一下響應(yīng)式編程(Reactive Programming)。
——也稱反應(yīng)式編程 _(:зゝ∠)_

你所不知道的響應(yīng)式編程
函數(shù)響應(yīng)式流庫探秘

好了,下面我們要開始進(jìn)行響應(yīng)式編程了。
首先,click事件可以形成一個(gè)“點(diǎn)擊流”,

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

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

其次,我們需要將這個(gè)“點(diǎn)擊流”,變換成最終的“ajax結(jié)果流”,
并且保證“ajax結(jié)果流”的順序,與“點(diǎn)擊流”的順序相同。

因此,問題在概念上就被簡(jiǎn)化了
事實(shí)上,所有的stream連同operator一起,構(gòu)成了一個(gè)Monad。

下面我們來編寫operator吧,用來對(duì)流進(jìn)行變換,我們只要記著,
什么時(shí)候調(diào)用cont就什么時(shí)候把東西放到結(jié)果流中”,即可。

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

    // 使用一個(gè)隊(duì)列和一個(gè)變量鎖來進(jìn)行調(diào)度
    // 如果當(dāng)前正在處理,就入隊(duì),否則就一次性清空隊(duì)列
    // 并且在清空的過程中,有了新的任務(wù)還可以入隊(duì)
    const queue = [];
    let isBusy = false;

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

        // 如果當(dāng)前正在處理,就返回,不改變結(jié)果stream中的值
        if (isBusy) {
            return;
        }

        // 否則就按順序處理,將每一個(gè)任務(wù)的返回值放到結(jié)果流中
        isBusy = true;
        while (queue.length !== 0) {
            const head = queue.shift();
            const r = await head();
            cont(r);
        }

        // 處理完了以后,恢復(fù)空閑狀態(tài)
        isBusy = false;
    });
};

我們?cè)賮砜聪略趺词褂盟?,是不是更加通俗易懂了呀?/p>

// 點(diǎn)擊流
const clickStream = cont => document.querySelector('#sendAjax').addEventListener('click', cont);

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

// 訂閱結(jié)果流
responseStream(v => console.log(v));

Your mouse is a database. —— Erik Meijer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,591評(píng)論 19 139
  • 五十三:請(qǐng)解釋 JavaScript 中 this 是如何工作的。1.方法調(diào)用模式當(dāng)一個(gè)函數(shù)被保存為一個(gè)對(duì)象的屬性...
    Arno_z閱讀 684評(píng)論 0 2
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,113評(píng)論 25 709
  • 十幾歲時(shí)的感情,回想起來,卻格外動(dòng)人。 那時(shí)候,還沒有多大的壓力,不用擔(dān)心房租,不用擔(dān)心由于加班趕不上末班車,不用...
    米恩閱讀 144評(píng)論 0 0
  • 前幾日讀了艾明雅的一篇文《身體都不喜歡,心還湊合個(gè)屁咧》,很受啟發(fā)。 女人是善妒的,由此女子之間...
    千譽(yù)嘉言閱讀 545評(píng)論 4 2

友情鏈接更多精彩內(nèi)容