
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è)按鈕的id為sendAjax,
<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