JavaScript隊(duì)列函數(shù)和異步執(zhí)行詳解
編輯注:在Review別人的JavaScript代碼時(shí)曾看到過(guò)類似的隊(duì)列函數(shù),不太理解,原來(lái)這個(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)來(lái)的,調(diào)用的時(shí)候并不知道都有些什么函數(shù);這個(gè)時(shí)候可以預(yù)先定義一個(gè)數(shù)組,添加函數(shù)的時(shí)候把函數(shù)push 進(jìn)去,需要的時(shí)候從數(shù)組中按順序一個(gè)個(gè)取出來(lái),依次調(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ù)有沒(méi)名字也不重要,直接把匿名函數(shù)傳進(jìn)去也可以。來(lái)測(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 中無(wú)法避免的一個(gè)話題,這里不打算探討JavaScript 中有關(guān)異步的各種術(shù)語(yǔ)和概念,請(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)用'
問(wèn)題很明顯,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)于把原來(lái)的fn2Timeout整個(gè)拿掉換成一個(gè)新函數(shù),再把原來(lái)的fn2Timeout和fn3插進(jìn)去。這種動(dòng)態(tài)改掉原函數(shù)的寫法有個(gè)專門的名詞叫Monkey Patch。按我們程序員的口頭禪:“做肯定是能做”,但寫起來(lái)有點(diǎn)擰巴,而且容易把自己繞進(jìn)去。有沒(méi)更好的做法?
我們退一步,不強(qiáng)求等f(wàn)n2Timeout完全執(zhí)行完才去執(zhí)行fn3,而是在fn2Timeout函數(shù)體的最后一行去調(diào)用:
function fn2() { setTimeout(function fn2Timeout() { console.log('第二個(gè)調(diào)用'); fn3(); // 注{1} }, 0); }
這樣看起來(lái)好了點(diǎn),不過(guò)定義fn2的時(shí)候都還沒(méi)有fn3,這fn3哪來(lái)的?
還有一個(gè)問(wèn)題,fn2里既然要調(diào)用fn3,那我們就不能通過(guò)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)在來(lái)實(shí)現(xiàn)next:
var index = 0; function next() { var fn = stack[index]; index = index + 1; // 其實(shí)也可以用shift 把fn 拿出來(lái) if (typeof fn === 'function') fn(); }
next通過(guò)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)用也沒(méi)用。 }); 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é)束,沒(méi)有任何機(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ì)心的你可能看出來(lái),這個(gè)next暫時(shí)只能放在函數(shù)的末尾,如果放在中間,原來(lái)的問(wèn)題還會(huì)出現(xiàn):
function fn() { console.log(1); next(); console.log(2); // next()如果調(diào)用了異步函數(shù),console.log(2)就會(huì)先執(zhí)行 }
redux 和koa 通過(guò)不同的實(shí)現(xiàn),可以讓next放在函數(shù)中間,執(zhí)行完后面的函數(shù)再折回來(lái)執(zhí)行next下面的代碼,非常巧妙。有空再寫寫。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
如何基于js管理大文件上傳及斷點(diǎn)續(xù)傳詳析
文件上傳是 Web 開(kāi)發(fā)肯定會(huì)碰到的問(wèn)題,而文件夾上傳則更加難,下面這篇文章主要給大家介紹了關(guān)于如何基于js管理大文件上傳及斷點(diǎn)續(xù)傳的相關(guān)資料,需要的朋友可以參考下2021-08-08JavaScript時(shí)間格式化函數(shù)功能及使用示例
這篇文章主要為大家介紹了JavaScript時(shí)間格式化函數(shù)功能及使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11D3.js 實(shí)現(xiàn)帶伸縮時(shí)間軸拓?fù)鋱D的示例代碼
這篇文章主要介紹了D3.js 實(shí)現(xiàn)帶伸縮時(shí)間軸拓?fù)鋱D的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01JS實(shí)現(xiàn)自動(dòng)變換的菜單效果代碼
這篇文章主要介紹了JS實(shí)現(xiàn)自動(dòng)變換的菜單效果代碼,可實(shí)現(xiàn)自動(dòng)變換菜單選中項(xiàng)的技巧,涉及JavaScript定時(shí)函數(shù)觸發(fā)頁(yè)面樣式屬性變換的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09JavaScript使用indexOf獲得子字符串在字符串中位置的方法
這篇文章主要介紹了JavaScript使用indexOf獲得子字符串在字符串中位置的方法,涉及javascript中indexOf方法操作字符串的技巧,需要的朋友可以參考下2015-04-04