JavaScript隊(duì)列函數(shù)和異步執(zhí)行詳解
編輯注:在Review別人的JavaScript代碼時(shí)曾看到過類似的隊(duì)列函數(shù),不太理解,原來這個(gè)是為了保證函數(shù)按順序調(diào)用。讀了這篇文章之后,發(fā)現(xiàn)還可以用在異步執(zhí)行等。
假設(shè)你有幾個(gè)函數(shù)fn1、fn2和fn3需要按順序調(diào)用,最簡(jiǎn)單的方式當(dāng)然是:
fn1(); fn2(); fn3();
但有時(shí)候這些函數(shù)是運(yùn)行時(shí)一個(gè)個(gè)添加進(jìn)來的,調(diào)用的時(shí)候并不知道都有些什么函數(shù);這個(gè)時(shí)候可以預(yù)先定義一個(gè)數(shù)組,添加函數(shù)的時(shí)候把函數(shù)push 進(jìn)去,需要的時(shí)候從數(shù)組中按順序一個(gè)個(gè)取出來,依次調(diào)用:
var stack = [];
// 執(zhí)行其他操作,定義fn1
stack.push(fn1);
// 執(zhí)行其他操作,定義fn2、fn3
stack.push(fn2, fn3);
// 調(diào)用的時(shí)候
stack.forEach(function(fn) { fn() });
這樣函數(shù)有沒名字也不重要,直接把匿名函數(shù)傳進(jìn)去也可以。來測(cè)試一下:
var stack = [];
function fn1() {
console.log('第一個(gè)調(diào)用');
}
stack.push(fn1);
function fn2() {
console.log('第二個(gè)調(diào)用');
}
stack.push(fn2, function() { console.log('第三個(gè)調(diào)用') });
stack.forEach(function(fn) { fn() }); // 按順序輸出'第一個(gè)調(diào)用'、'第二個(gè)調(diào)用'、'第三個(gè)調(diào)用'
這個(gè)實(shí)現(xiàn)目前為止工作正常,但我們忽略了一個(gè)情況,就是異步函數(shù)的調(diào)用。異步是JavaScript 中無法避免的一個(gè)話題,這里不打算探討JavaScript 中有關(guān)異步的各種術(shù)語和概念,請(qǐng)讀者自行查閱(例如某篇著名的評(píng)注)。如果你知道下面代碼會(huì)輸出1、3、2,那請(qǐng)繼續(xù)往下看:
console.log(1);
setTimeout(function() {
console.log(2);
}, 0);
console.log(3);
假如stack 隊(duì)列中有某個(gè)函數(shù)是類似的異步函數(shù),我們的實(shí)現(xiàn)就亂套了:
var stack = [];
function fn1() { console.log('第一個(gè)調(diào)用') };
stack.push(fn1);
function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二個(gè)調(diào)用');
}, 0);
}
stack.push(fn2, function() { console.log('第三個(gè)調(diào)用') });
stack.forEach(function(fn) { fn() }); // 輸出'第一個(gè)調(diào)用'、'第三個(gè)調(diào)用'、'第二個(gè)調(diào)用'
問題很明顯,fn2確實(shí)按順序調(diào)用了,但setTimeout里的function fn2Timeout() { console.log(‘第二個(gè)調(diào)用') }卻不是立即執(zhí)行的(即使把timeout 設(shè)為0);fn2調(diào)用之后馬上返回,接著執(zhí)行fn3,fn3執(zhí)行完了然才真正輪到fn2Timeout。
怎么解決?我們分析下,這里的關(guān)鍵在于fn2Timeout,我們必須等到它真正執(zhí)行完才調(diào)用fn3,理想情況下大概像這樣:
function fn2() {
setTimeout(function() {
fn2Timeout();
fn3();
}, 0);
}
但這樣做相當(dāng)于把原來的fn2Timeout整個(gè)拿掉換成一個(gè)新函數(shù),再把原來的fn2Timeout和fn3插進(jìn)去。這種動(dòng)態(tài)改掉原函數(shù)的寫法有個(gè)專門的名詞叫Monkey Patch。按我們程序員的口頭禪:“做肯定是能做”,但寫起來有點(diǎn)擰巴,而且容易把自己繞進(jìn)去。有沒更好的做法?
我們退一步,不強(qiáng)求等fn2Timeout完全執(zhí)行完才去執(zhí)行fn3,而是在fn2Timeout函數(shù)體的最后一行去調(diào)用:
function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二個(gè)調(diào)用');
fn3(); // 注{1}
}, 0);
}
這樣看起來好了點(diǎn),不過定義fn2的時(shí)候都還沒有fn3,這fn3哪來的?
還有一個(gè)問題,fn2里既然要調(diào)用fn3,那我們就不能通過stack.forEach去調(diào)用fn3了,否則fn3會(huì)重復(fù)調(diào)用兩次。
我們不能把fn3寫死在fn2里。相反,我們只需要在fn2Timeout末尾里找出stack中fn2的下一個(gè)函數(shù),再調(diào)用:
function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二個(gè)調(diào)用');
next();
}, 0);
}
這個(gè)next函數(shù)負(fù)責(zé)找出stack 中的下一個(gè)函數(shù)并執(zhí)行。我們現(xiàn)在來實(shí)現(xiàn)next:
var index = 0;
function next() {
var fn = stack[index];
index = index + 1; // 其實(shí)也可以用shift 把fn 拿出來
if (typeof fn === 'function') fn();
}
next通過stack[index]去獲取stack中的函數(shù),每調(diào)用next一次index會(huì)加1,從而達(dá)到取出下一個(gè)函數(shù)的目的。
next這樣使用:
var stack = [];
// 定義index 和next
function fn1() {
console.log('第一個(gè)調(diào)用');
next(); // stack 中每一個(gè)函數(shù)都必須調(diào)用`next`
};
stack.push(fn1);
function fn2() {
setTimeout(function fn2Timeout() {
console.log('第二個(gè)調(diào)用');
next(); // 調(diào)用`next`
}, 0);
}
stack.push(fn2, function() {
console.log('第三個(gè)調(diào)用');
next(); // 最后一個(gè)可以不調(diào)用,調(diào)用也沒用。
});
next(); // 調(diào)用next,最終按順序輸出'第一個(gè)調(diào)用'、'第二個(gè)調(diào)用'、'第三個(gè)調(diào)用'。
現(xiàn)在stack.forEach一行已經(jīng)刪掉了,我們自行調(diào)用一次next,next會(huì)找出stack中的第一個(gè)函數(shù)fn1執(zhí)行,fn1 里調(diào)用next,去找出下一個(gè)函數(shù)fn2并執(zhí)行,fn2里再調(diào)用next,依此類推。
每一個(gè)函數(shù)里都必須調(diào)用next,如果某個(gè)函數(shù)里不寫,執(zhí)行完該函數(shù)后程序就會(huì)直接結(jié)束,沒有任何機(jī)制繼續(xù)。
了解了函數(shù)隊(duì)列的這個(gè)實(shí)現(xiàn)后,你應(yīng)該可以解決下面這道面試題了:
// 實(shí)現(xiàn)一個(gè)LazyMan,可以按照以下方式調(diào)用: LazyMan(“Hank”) /* 輸出: Hi! This is Hank! */ LazyMan(“Hank”).sleep(10).eat(“dinner”)輸出 /* 輸出: Hi! This is Hank! // 等待10秒.. Wake up after 10 Eat dinner~ */ LazyMan(“Hank”).eat(“dinner”).eat(“supper”) /* 輸出: Hi This is Hank! Eat dinner~ Eat supper~ */ LazyMan(“Hank”).sleepFirst(5).eat(“supper”) /* 等待5秒,輸出 Wake up after 5 Hi This is Hank! Eat supper */ // 以此類推。
Node.js 中大名鼎鼎的connect框架正是這樣實(shí)現(xiàn)中間件隊(duì)列的。有興趣可以去看看它的源碼或者這篇解讀《何為 connect 中間件》。
細(xì)心的你可能看出來,這個(gè)next暫時(shí)只能放在函數(shù)的末尾,如果放在中間,原來的問題還會(huì)出現(xiàn):
function fn() {
console.log(1);
next();
console.log(2); // next()如果調(diào)用了異步函數(shù),console.log(2)就會(huì)先執(zhí)行
}
redux 和koa 通過不同的實(shí)現(xiàn),可以讓next放在函數(shù)中間,執(zhí)行完后面的函數(shù)再折回來執(zhí)行next下面的代碼,非常巧妙。有空再寫寫。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
JS實(shí)現(xiàn)標(biāo)簽滾動(dòng)切換效果
這篇文章給大家?guī)淼氖怯肑S實(shí)現(xiàn)item標(biāo)簽點(diǎn)擊后滾動(dòng)切換的效果,有興趣的朋友測(cè)試學(xué)習(xí)下吧。2017-12-12
JavaScript中sharedWorker 實(shí)現(xiàn)多頁(yè)面通信的實(shí)例詳解
這篇文章主要介紹了JavaScript中sharedWorker 實(shí)現(xiàn)多頁(yè)面通信,通過給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04
細(xì)品javascript 尋址,閉包,對(duì)象模型和相關(guān)問題
似乎某些程序員的集合是不相交的,就好像JS程序員和玩編譯原理和CPU指令的匯編程序員就幾乎沒有交叉。前些日子討論的火熱的“作用域鏈”問題,說白了就是尋址問題,不過,這個(gè)在C中十分簡(jiǎn)單的問題卻被JS這個(gè)動(dòng)態(tài)語言弄得很復(fù)雜。2009-04-04
兩種方法實(shí)現(xiàn)在HTML頁(yè)面加載完畢后運(yùn)行某個(gè)js
這篇文章主要介紹了通過兩種方法實(shí)現(xiàn)在HTML頁(yè)面加載完畢后運(yùn)行某個(gè)js,需要的朋友可以參考下2014-06-06
JS動(dòng)態(tài)添加的div點(diǎn)擊跳轉(zhuǎn)到另一頁(yè)面實(shí)現(xiàn)代碼
這篇文章主要介紹了JS動(dòng)態(tài)添加的div點(diǎn)擊跳轉(zhuǎn)到另一頁(yè)面實(shí)現(xiàn)代碼,需要的朋友可以參考下2017-09-09

