您好,登錄后才能下訂單哦!
編輯注:在Review別人的JavaScript代碼時曾看到過類似的隊列函數(shù),不太理解,原來這個是為了保證函數(shù)按順序調(diào)用。讀了這篇文章之后,發(fā)現(xiàn)還可以用在異步執(zhí)行等。
假設(shè)你有幾個函數(shù)fn1、fn2和fn3需要按順序調(diào)用,最簡單的方式當(dāng)然是:
fn1(); fn2(); fn3();
但有時候這些函數(shù)是運(yùn)行時一個個添加進(jìn)來的,調(diào)用的時候并不知道都有些什么函數(shù);這個時候可以預(yù)先定義一個數(shù)組,添加函數(shù)的時候把函數(shù)push 進(jìn)去,需要的時候從數(shù)組中按順序一個個取出來,依次調(diào)用:
var stack = []; // 執(zhí)行其他操作,定義fn1 stack.push(fn1); // 執(zhí)行其他操作,定義fn2、fn3 stack.push(fn2, fn3); // 調(diào)用的時候 stack.forEach(function(fn) { fn() });
這樣函數(shù)有沒名字也不重要,直接把匿名函數(shù)傳進(jìn)去也可以。來測試一下:
var stack = []; function fn1() { console.log('第一個調(diào)用'); } stack.push(fn1); function fn2() { console.log('第二個調(diào)用'); } stack.push(fn2, function() { console.log('第三個調(diào)用') }); stack.forEach(function(fn) { fn() }); // 按順序輸出'第一個調(diào)用'、'第二個調(diào)用'、'第三個調(diào)用'
這個實(shí)現(xiàn)目前為止工作正常,但我們忽略了一個情況,就是異步函數(shù)的調(diào)用。異步是JavaScript 中無法避免的一個話題,這里不打算探討JavaScript 中有關(guān)異步的各種術(shù)語和概念,請讀者自行查閱(例如某篇著名的評注)。如果你知道下面代碼會輸出1、3、2,那請繼續(xù)往下看:
console.log(1); setTimeout(function() { console.log(2); }, 0); console.log(3);
假如stack 隊列中有某個函數(shù)是類似的異步函數(shù),我們的實(shí)現(xiàn)就亂套了:
var stack = []; function fn1() { console.log('第一個調(diào)用') }; stack.push(fn1); function fn2() { setTimeout(function fn2Timeout() { console.log('第二個調(diào)用'); }, 0); } stack.push(fn2, function() { console.log('第三個調(diào)用') }); stack.forEach(function(fn) { fn() }); // 輸出'第一個調(diào)用'、'第三個調(diào)用'、'第二個調(diào)用'
問題很明顯,fn2確實(shí)按順序調(diào)用了,但setTimeout里的function fn2Timeout() { console.log(‘第二個調(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整個拿掉換成一個新函數(shù),再把原來的fn2Timeout和fn3插進(jìn)去。這種動態(tài)改掉原函數(shù)的寫法有個專門的名詞叫Monkey Patch。按我們程序員的口頭禪:“做肯定是能做”,但寫起來有點(diǎn)擰巴,而且容易把自己繞進(jìn)去。有沒更好的做法?
我們退一步,不強(qiáng)求等fn2Timeout完全執(zhí)行完才去執(zhí)行fn3,而是在fn2Timeout函數(shù)體的最后一行去調(diào)用:
function fn2() { setTimeout(function fn2Timeout() { console.log('第二個調(diào)用'); fn3(); // 注{1} }, 0); }
這樣看起來好了點(diǎn),不過定義fn2的時候都還沒有fn3,這fn3哪來的?
還有一個問題,fn2里既然要調(diào)用fn3,那我們就不能通過stack.forEach去調(diào)用fn3了,否則fn3會重復(fù)調(diào)用兩次。
我們不能把fn3寫死在fn2里。相反,我們只需要在fn2Timeout末尾里找出stack中fn2的下一個函數(shù),再調(diào)用:
function fn2() { setTimeout(function fn2Timeout() { console.log('第二個調(diào)用'); next(); }, 0); }
這個next函數(shù)負(fù)責(zé)找出stack 中的下一個函數(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會加1,從而達(dá)到取出下一個函數(shù)的目的。
next這樣使用:
var stack = []; // 定義index 和next function fn1() { console.log('第一個調(diào)用'); next(); // stack 中每一個函數(shù)都必須調(diào)用`next` }; stack.push(fn1); function fn2() { setTimeout(function fn2Timeout() { console.log('第二個調(diào)用'); next(); // 調(diào)用`next` }, 0); } stack.push(fn2, function() { console.log('第三個調(diào)用'); next(); // 最后一個可以不調(diào)用,調(diào)用也沒用。 }); next(); // 調(diào)用next,最終按順序輸出'第一個調(diào)用'、'第二個調(diào)用'、'第三個調(diào)用'。
現(xiàn)在stack.forEach一行已經(jīng)刪掉了,我們自行調(diào)用一次next,next會找出stack中的第一個函數(shù)fn1執(zhí)行,fn1 里調(diào)用next,去找出下一個函數(shù)fn2并執(zhí)行,fn2里再調(diào)用next,依此類推。
每一個函數(shù)里都必須調(diào)用next,如果某個函數(shù)里不寫,執(zhí)行完該函數(shù)后程序就會直接結(jié)束,沒有任何機(jī)制繼續(xù)。
了解了函數(shù)隊列的這個實(shí)現(xiàn)后,你應(yīng)該可以解決下面這道面試題了:
// 實(shí)現(xiàn)一個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)中間件隊列的。有興趣可以去看看它的源碼或者這篇解讀《何為 connect 中間件》。
細(xì)心的你可能看出來,這個next暫時只能放在函數(shù)的末尾,如果放在中間,原來的問題還會出現(xiàn):
function fn() { console.log(1); next(); console.log(2); // next()如果調(diào)用了異步函數(shù),console.log(2)就會先執(zhí)行 }
redux 和koa 通過不同的實(shí)現(xiàn),可以讓next放在函數(shù)中間,執(zhí)行完后面的函數(shù)再折回來執(zhí)行next下面的代碼,非常巧妙。有空再寫寫。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。