溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JavaScript隊列函數(shù)和異步執(zhí)行詳解

發(fā)布時間:2020-10-05 17:24:52 來源:腳本之家 閱讀:156 作者:hehekai 欄目:web開發(fā)

編輯注:在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í)有所幫助,也希望大家多多支持億速云。

向AI問一下細(xì)節(jié)

免責(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)容。

AI