假設你有幾個函數fn1
、fn2
和fn3
須要按順序調用,最簡單的方式固然是:node
fn1(); fn2(); fn3();
但有時候這些函數是運行時一個個添加進來的,調用的時候並不知道都有些什麼函數;這個時候能夠預先定義一個數組,添加函數的時候把函數push 進去,須要的時候從數組中按順序一個個取出來,依次調用:git
var stack = []; // 執行其餘操做,定義fn1 stack.push(fn1); // 執行其餘操做,定義fn二、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 中有關異步的各類術語和概念,請讀者自行查閱(例如某篇著名的評註)。若是你知道下面代碼會輸出一、三、2,那請繼續往下看:github
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
,理想狀況下大概像這樣:redux
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
會重複調用兩次。less
咱們不能把fn3
寫死在fn2
裏。相反,咱們只須要在fn2Timeout
末尾裏找出stack
中fn2
的下一個函數,再調用:koa
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
下面的代碼,很是巧妙。有空再寫寫。