摘要:相反,我們只需要在末尾里找出中的下一個函數,再調用第二個調用這個函數負責找出中的下一個函數并執行。我們現在來實現其實也可以用把拿出來通過去獲取中的函數,每調用一次會加,從而達到取出下一個函數的目的。中大名鼎鼎的框架正是這樣實現中間件隊列的。
假設你有幾個函數fn1、fn2和fn3需要按順序調用,最簡單的方式當然是:
fn1(); fn2(); fn3();
但有時候這些函數是運行時一個個添加進來的,調用的時候并不知道都有些什么函數;這個時候可以預先定義一個數組,添加函數的時候把函數push 進去,需要的時候從數組中按順序一個個取出來,依次調用:
var stack = []; // 執行其他操作,定義fn1 stack.push(fn1); // 執行其他操作,定義fn2、fn3 stack.push(fn2, fn3); // 調用的時候 stack.forEach(function(fn) { fn() });
這樣函數有沒名字也不重要,直接把匿名函數傳進去也可以。來測試一下:
var stack = []; function fn1() { console.log("第一個調用"); } stack.push(fn1); function fn2() { console.log("第二個調用"); } stack.push(fn2, function() { console.log("第三個調用") }); stack.forEach(function(fn) { fn() }); // 按順序輸出"第一個調用"、"第二個調用"、"第三個調用"
這個實現目前為止工作正常,但我們忽略了一個情況,就是異步函數的調用。異步是JavaScript 中無法避免的一個話題,這里不打算探討JavaScript 中有關異步的各種術語和概念,請讀者自行查閱(例如某篇著名的評注)。如果你知道下面代碼會輸出1、3、2,那請繼續往下看:
console.log(1); setTimeout(function() { console.log(2); }, 0); console.log(3);
假如stack 隊列中有某個函數是類似的異步函數,我們的實現就亂套了:
var stack = []; function fn1() { console.log("第一個調用") }; stack.push(fn1); function fn2() { setTimeout(function fn2Timeout() { console.log("第二個調用"); }, 0); } stack.push(fn2, function() { console.log("第三個調用") }); stack.forEach(function(fn) { fn() }); // 輸出"第一個調用"、"第三個調用"、"第二個調用"
問題很明顯,fn2確實按順序調用了,但setTimeout里的function fn2Timeout() { console.log("第二個調用") }卻不是立即執行的(即使把timeout 設為0);fn2調用之后馬上返回,接著執行fn3,fn3執行完了然才真正輪到fn2Timeout。
怎么解決?我們分析下,這里的關鍵在于fn2Timeout,我們必須等到它真正執行完才調用fn3,理想情況下大概像這樣:
function fn2() { setTimeout(function() { fn2Timeout(); fn3(); }, 0); }
但這樣做相當于把原來的fn2Timeout整個拿掉換成一個新函數,再把原來的fn2Timeout和fn3插進去。這種動態改掉原函數的寫法有個專門的名詞叫Monkey Patch。按我們程序員的口頭禪:“做肯定是能做”,但寫起來有點擰巴,而且容易把自己繞進去。有沒更好的做法?
我們退一步,不強求等fn2Timeout完全執行完才去執行fn3,而是在fn2Timeout函數體的最后一行去調用:
function fn2() { setTimeout(function fn2Timeout() { console.log("第二個調用"); fn3(); // 注{1} }, 0); }
這樣看起來好了點,不過定義fn2的時候都還沒有fn3,這fn3哪來的?
還有一個問題,fn2里既然要調用fn3,那我們就不能通過stack.forEach去調用fn3了,否則fn3會重復調用兩次。
我們不能把fn3寫死在fn2里。相反,我們只需要在fn2Timeout末尾里找出stack中fn2的下一個函數,再調用:
function fn2() { setTimeout(function fn2Timeout() { console.log("第二個調用"); next(); }, 0); }
這個next函數負責找出stack 中的下一個函數并執行。我們現在來實現next:
var index = 0; function next() { var fn = stack[index]; index = index + 1; // 其實也可以用shift 把fn 拿出來 if (typeof fn === "function") fn(); }
next通過stack[index]去獲取stack中的函數,每調用next一次index會加1,從而達到取出下一個函數的目的。
next這樣使用:
var stack = []; // 定義index 和next function fn1() { console.log("第一個調用"); next(); // stack 中每一個函數都必須調用`next` }; stack.push(fn1); function fn2() { setTimeout(function fn2Timeout() { console.log("第二個調用"); next(); // 調用`next` }, 0); } stack.push(fn2, function() { console.log("第三個調用"); next(); // 最后一個可以不調用,調用也沒用。 }); next(); // 調用next,最終按順序輸出"第一個調用"、"第二個調用"、"第三個調用"。
現在stack.forEach一行已經刪掉了,我們自行調用一次next,next會找出stack中的第一個函數fn1執行,fn1 里調用next,去找出下一個函數fn2并執行,fn2里再調用next,依此類推。
每一個函數里都必須調用next,如果某個函數里不寫,執行完該函數后程序就會直接結束,沒有任何機制繼續。
了解了函數隊列的這個實現后,你應該可以解決下面這道面試題了:
// 實現一個LazyMan,可以按照以下方式調用: 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框架正是這樣實現中間件隊列的。有興趣可以去看看它的源碼或者這篇解讀《何為 connect 中間件》。
細心的你可能看出來,這個next暫時只能放在函數的末尾,如果放在中間,原來的問題還會出現:
function fn() { console.log(1); next(); console.log(2); // next()如果調用了異步函數,console.log(2)就會先執行 }
redux 和koa 通過不同的實現,可以讓next放在函數中間,執行完后面的函數再折回來執行next下面的代碼,非常巧妙。有空再寫寫。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81491.html
摘要:這兩個函數接受定時器的例如我們上面提到的兩個函數產生的定時器,并停止對定時器中指定函數的調用。注意,定時器雖然觸發了,但是并不會立即執行,它只是把需要延遲執行的函數加入了執行隊列,在線程的某一個可用的時間點,這個函數就能夠得到執行。 擼了今年阿里、頭條和美團的面試,我有一個重要發現....... javascript定時器工作原理是一個重要的基礎知識點。因為定時器在單線程中工作,它們表...
摘要:最受歡迎的引擎是,在和中使用,用于,以及所使用的。單線程的我們說是單線程的,因為有一個調用棧處理我們的函數。也就是說,如果有其他函數等待執行,函數是不能離開調用棧的。每個異步函數在被送入調用棧之前必須通過回調隊列。 翻譯:瘋狂的技術宅原文:https://www.valentinog.com/bl... 本文首發微信公眾號:前端先鋒歡迎關注,每天都給你推送新鮮的前端技術文章 sh...
摘要:而事件循環是主線程中執行棧里的代碼執行完畢之后,才開始執行的。由此產生的異步事件執行會作為任務隊列掛在當前循環的末尾執行。在下,觀察者基于監聽事件的完成情況在下基于多線程創建。 主要問題: 1、JS引擎是單線程,如何完成事件循環的? 2、定時器函數為什么計時不準確? 3、回調與異步,有什么聯系和不同? 4、ES6的事件循環有什么變化?Node中呢? 5、異步控制有什么難點?有什么解決方...
閱讀 2106·2021-11-05 09:42
閱讀 2851·2021-09-23 11:21
閱讀 2841·2019-08-30 14:00
閱讀 3314·2019-08-30 13:15
閱讀 465·2019-08-29 17:18
閱讀 3547·2019-08-29 16:29
閱讀 2749·2019-08-29 14:06
閱讀 2794·2019-08-23 14:41