你不懂JS: 異步與性能 第一章: 異步: 現(xiàn)在與稍后

官方中文版原文鏈接

感謝社區(qū)中各位的大力支持芜飘,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券衣赶,享受所有官網(wǎng)優(yōu)惠伍茄,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取

在像JavaScript這樣的語(yǔ)言中最重要但經(jīng)常被誤解的編程技術(shù)之一淹朋,就是如何表達(dá)和操作跨越一段時(shí)間的程序行為偏塞。

這不僅僅是關(guān)于從for循環(huán)開(kāi)始到for循環(huán)結(jié)束之間發(fā)生的事情唱蒸,當(dāng)然它確實(shí)要花 一些時(shí)間(幾微秒到幾毫秒)才能完成。它是關(guān)于你的程序 現(xiàn)在 運(yùn)行的部分灸叼,和你的程序 稍后 運(yùn)行的另一部分之間發(fā)生的事情——現(xiàn)在稍后 之間有一個(gè)間隙神汹,在這個(gè)間隙中你的程序沒(méi)有活躍地執(zhí)行。

幾乎所有被編寫(xiě)過(guò)的(特別是用JS)大型程序都不得不用這樣或那樣的方法來(lái)管理這個(gè)間隙古今,不管是等待用戶輸入屁魏,從數(shù)據(jù)庫(kù)或文件系統(tǒng)請(qǐng)求數(shù)據(jù),通過(guò)網(wǎng)絡(luò)發(fā)送數(shù)據(jù)并等待應(yīng)答捉腥,還是在規(guī)定的時(shí)間間隔重復(fù)某些任務(wù)(比如動(dòng)畫(huà))氓拼。在所有這些各種方法中,你的程序都不得不跨越時(shí)間間隙管理狀態(tài)抵碟。就像在倫敦眾所周知的一句話(地鐵門(mén)與月臺(tái)間的縫隙):“小心間隙御滩『荔荩”

實(shí)際上,你程序中 現(xiàn)在稍后 的部分之間的關(guān)系,就是異步編程的核心盏缤。

可以確定的是,異步編程在JS的最開(kāi)始就出現(xiàn)了斧散。但是大多數(shù)開(kāi)發(fā)者從沒(méi)認(rèn)真地考慮過(guò)它到底是如何佳魔,為什么出現(xiàn)在他們的程序中的,也沒(méi)有探索過(guò) 其他 處理異步的方式罚屋。足夠好 的方法總是老實(shí)巴交的回調(diào)函數(shù)苦囱。今天還有許多人堅(jiān)持認(rèn)為回調(diào)就綽綽有余了。

但是JS在使用范圍和復(fù)雜性上不停地生長(zhǎng)沿后,作為運(yùn)行在瀏覽器沿彭,服務(wù)器和每種可能的設(shè)備上的頭等編程語(yǔ)言,為了適應(yīng)它不斷擴(kuò)大的要求尖滚,我們?cè)诠芾懋惒缴细惺艿降耐纯嗳遮厙?yán)重喉刘,人們迫切地需要一種更強(qiáng)大更合理的處理方法。

雖然眼前這一切看起來(lái)很抽象漆弄,但我保證睦裳,隨著我們通讀這本書(shū)你會(huì)更完整且堅(jiān)實(shí)地解決它。在接下來(lái)的幾章中我們將會(huì)探索各種異步JavaScript編程的新興技術(shù)撼唾。

但在接觸它們之前廉邑,我們將不得不更深刻地理解異步是什么,以及它在JS中如何運(yùn)行。

塊兒(Chunks)中的程序

你可能將你的JS程序?qū)懺谝粋€(gè) .js 文件中蛛蒙,但幾乎可以確定你的程序是由幾個(gè)代碼塊兒構(gòu)成的糙箍,僅有其中的一個(gè)將會(huì)在 現(xiàn)在 執(zhí)行,而其他的將會(huì)在 稍后 執(zhí)行牵祟。最常見(jiàn)的 代碼塊兒 單位是function深夯。

大多數(shù)剛接觸JS的開(kāi)發(fā)者都可能會(huì)有的問(wèn)題是,稍后 并不嚴(yán)格且立即地在 現(xiàn)在 之后發(fā)生诺苹。換句話說(shuō)咕晋,根據(jù)定義,現(xiàn)在 不能完成的任務(wù)將會(huì)異步地完成收奔,而且我們因此不會(huì)有你可能在直覺(jué)上期望或想要的阻塞行為掌呜。

考慮這段代碼:

// ajax(..)是某個(gè)包中任意的Ajax函數(shù)
var data = ajax( "http://some.url.1" );

console.log( data );
// 噢!`data`一般不會(huì)有Ajax的結(jié)果

你可能意識(shí)到Ajax請(qǐng)求不會(huì)同步地完成坪哄,這意味著ajax(..)函數(shù)還沒(méi)有任何返回的值可以賦值給變量data质蕉。如果ajax(..)在應(yīng)答返回之前 能夠 阻塞,那么data = ..賦值將會(huì)正常工作损姜。

但那不是我們使用Ajax的方式饰剥。我們 現(xiàn)在 制造一個(gè)異步的Ajax請(qǐng)求,直到 稍后 我們才會(huì)得到結(jié)果摧阅。

現(xiàn)在 “等到” 稍后 最簡(jiǎn)單的(但絕對(duì)不是唯一的汰蓉,或最好的)方法,通常稱為回調(diào)函數(shù):

// ajax(..) 是某個(gè)包中任意的Ajax函數(shù)
ajax( "http://some.url.1", function myCallbackFunction(data){

    console.log( data ); // Yay, 我得到了一些`data`!

} );

警告: 你可能聽(tīng)說(shuō)過(guò)發(fā)起同步的Ajax請(qǐng)求是可能的棒卷。雖然在技術(shù)上是這樣的顾孽,但你永遠(yuǎn),永遠(yuǎn)不應(yīng)該在任何情況下這樣做比规,因?yàn)樗鼘㈡i定瀏覽器的UI(按鈕若厚,菜單,滾動(dòng)條蜒什,等等)而且阻止用戶與任何東西互動(dòng)测秸。這是一個(gè)非常差勁的主意,你應(yīng)當(dāng)永遠(yuǎn)回避它灾常。

在你提出抗議之前霎冯,不,你渴望避免混亂的回調(diào)不是使用阻塞的钞瀑,同步的Ajax的正當(dāng)理由沈撞。

舉個(gè)例子,考慮下面的代碼:

function now() {
    return 21;
}

function later() {
    answer = answer * 2;
    console.log( "Meaning of life:", answer );
}

var answer = now();

setTimeout( later, 1000 ); // Meaning of life: 42

這個(gè)程序中有兩個(gè)代碼塊兒:現(xiàn)在 將會(huì)運(yùn)行的東西雕什,和 稍后 將會(huì)運(yùn)行的東西缠俺。這兩個(gè)代碼塊分別是什么應(yīng)當(dāng)十分明顯显晶,但還是讓我們以最明確的方式指出來(lái):

現(xiàn)在:

function now() {
    return 21;
}

function later() { .. }

var answer = now();

setTimeout( later, 1000 );

稍后:

answer = answer * 2;
console.log( "Meaning of life:", answer );

你的程序一執(zhí)行,現(xiàn)在 代碼塊兒就會(huì)立即運(yùn)行壹士。但setTimeout(..)還設(shè)置了一個(gè) 稍后 會(huì)發(fā)生的事件(一個(gè)超時(shí)事件)磷雇,所以later()函數(shù)的內(nèi)容將會(huì)在一段時(shí)間后(從現(xiàn)在開(kāi)始1000毫秒)被執(zhí)行。

每當(dāng)你將一部分代碼包進(jìn)function并且規(guī)定它應(yīng)當(dāng)為了響應(yīng)某些事件而執(zhí)行(定時(shí)器墓卦,鼠標(biāo)點(diǎn)擊倦春,Ajax應(yīng)答等等),你就創(chuàng)建了一個(gè) 稍后 代碼塊兒落剪,也因此在你的程序中引入了異步。

異步控制臺(tái)

關(guān)于console.*方法如何工作尿庐,沒(méi)有相應(yīng)的語(yǔ)言規(guī)范或一組需求——它們不是JavaScript官方的一部分忠怖,而是由 宿主環(huán)境 添加到JS上的(見(jiàn)本叢書(shū)的 類型與文法)。

所以抄瑟,不同的瀏覽器和JS環(huán)境各自為戰(zhàn)凡泣,這有時(shí)會(huì)導(dǎo)致令人困惑的行為。

特別地皮假,有些瀏覽器和某些條件下鞋拟,console.log(..)實(shí)際上不會(huì)立即輸出它得到的東西。這個(gè)現(xiàn)象的主要原因可能是因?yàn)镮/O處理很慢惹资,而且是許多程序的阻塞部分(不僅是JS)贺纲。所以,對(duì)一個(gè)瀏覽器來(lái)說(shuō)褪测,可能的性能更好的處理方式是(從網(wǎng)頁(yè)/UI的角度看)猴誊,在后臺(tái)異步地處理consoleI/O,而你也許根本不知道它發(fā)生了侮措。

雖然不是很常見(jiàn)懈叹,但是一種可能被觀察到(不是從代碼本身,而是從外部)的場(chǎng)景是:

var a = {
    index: 1
};

// 稍后
console.log( a ); // ??

// 再稍后
a.index++;

我們一般希望看到的是分扎,就在console.log(..)語(yǔ)句被執(zhí)行的那一刻澄成,對(duì)象a被取得一個(gè)快照,打印出如{ index: 1 }的內(nèi)容畏吓,如此在下一個(gè)語(yǔ)句a.index++執(zhí)行時(shí)墨状,它修改不同于a的輸出,或者嚴(yán)格的在a的輸出之后的某些東西庵佣。

大多數(shù)時(shí)候歉胶,上面的代碼將會(huì)在你的開(kāi)發(fā)者工具控制臺(tái)中產(chǎn)生一個(gè)你期望的對(duì)象表現(xiàn)形式。但是同樣的代碼也可能運(yùn)行在這樣的情況下:瀏覽器告訴后臺(tái)它需要推遲控制臺(tái)I/O巴粪,這時(shí)通今,在對(duì)象在控制臺(tái)中被表示的那個(gè)時(shí)間點(diǎn)粥谬,a.index++已經(jīng)執(zhí)行了,所以它將顯示{ index: 2 }辫塌。

到底在什么條件下consoleI/O將被推遲是不確定的漏策,甚至它能不能被觀察到都是不確定的。只能當(dāng)你在調(diào)試過(guò)程中遇到問(wèn)題時(shí)——對(duì)象在console.log(..)語(yǔ)句之后被修改臼氨,但你卻意外地看到了修改后的內(nèi)容——意識(shí)到I/O的這種可能的異步性掺喻。

注意: 如果你遇到了這種罕見(jiàn)的情況,最好的選擇是使用JS調(diào)試器的斷點(diǎn)储矩,而不是依賴console的輸出感耙。第二好的選擇是通過(guò)將目標(biāo)對(duì)象序列化為一個(gè)string強(qiáng)制取得一個(gè)它的快照,比如用JSON.stringify(..)持隧。

事件輪詢(Event Loop)

讓我們來(lái)做一個(gè)(也許是令人震驚的)聲明:盡管明確地允許異步JS代碼(就像我們剛看到的超時(shí))即硼,但是實(shí)際上,直到最近(ES6)為止屡拨,JavaScript本身從來(lái)沒(méi)有任何內(nèi)建的異步概念只酥。

什么!呀狼? 這聽(tīng)起來(lái)簡(jiǎn)直是瘋了裂允,對(duì)吧?事實(shí)上哥艇,它是真的绝编。JS引擎本身除了在某個(gè)在被要求的時(shí)刻執(zhí)行你程序的一個(gè)單獨(dú)的代碼塊外,沒(méi)有做過(guò)任何其他的事情她奥。

“被'誰(shuí)'要求”瓮增?這才是重要的部分!

JS引擎沒(méi)有運(yùn)行在隔離的區(qū)域哩俭。它運(yùn)行在一個(gè) 宿主環(huán)境 中绷跑,對(duì)大多數(shù)開(kāi)發(fā)者來(lái)說(shuō)這個(gè)宿主環(huán)境就是瀏覽器。在過(guò)去的幾年中(但不特指這幾年)凡资,JS超越了瀏覽器的界限進(jìn)入到了其他環(huán)境中砸捏,比如服務(wù)器,通過(guò)Node.js這樣的東西隙赁。其實(shí)垦藏,今天JavaScript已經(jīng)被嵌入到所有種類的設(shè)備中,從機(jī)器人到電燈泡兒伞访。

所有這些環(huán)境的一個(gè)共通的“線程”(一個(gè)“不那么微妙”的異步玩笑掂骏,不管怎樣)是,他們都有一種機(jī)制:在每次調(diào)用JS引擎時(shí)厚掷,可以 隨著時(shí)間的推移 執(zhí)行你的程序的多個(gè)代碼塊兒弟灼,這稱為“事件輪詢(Event Loop)”级解。

換句話說(shuō),JS引擎對(duì) 時(shí)間 沒(méi)有天生的感覺(jué)田绑,反而是一個(gè)任意JS代碼段的按需執(zhí)行環(huán)境勤哗。是它周?chē)沫h(huán)境在不停地安排“事件”(JS代碼的執(zhí)行)。

那么掩驱,舉例來(lái)說(shuō)芒划,當(dāng)你的JS程序發(fā)起一個(gè)從服務(wù)器取得數(shù)據(jù)的Ajax請(qǐng)求時(shí),你在一個(gè)函數(shù)(通常稱為回調(diào))中建立好“應(yīng)答”代碼欧穴,然后JS引擎就會(huì)告訴宿主環(huán)境民逼,“嘿,我就要暫時(shí)停止執(zhí)行了苔可,但不管你什么時(shí)候完成了這個(gè)網(wǎng)絡(luò)請(qǐng)求缴挖,而且你還得到一些數(shù)據(jù)的話,請(qǐng) 回來(lái)調(diào) 這個(gè)函數(shù)焚辅。”

然后瀏覽器就會(huì)為網(wǎng)絡(luò)的應(yīng)答設(shè)置一個(gè)監(jiān)聽(tīng)器苟鸯,當(dāng)它有東西要交給你的時(shí)候同蜻,它會(huì)通過(guò)將回調(diào)函數(shù)插入 事件輪詢 來(lái)安排它的執(zhí)行。

那么什么是 事件輪詢早处?

讓我們先通過(guò)一些假想代碼來(lái)對(duì)它形成一個(gè)概念:

// `eventLoop`是一個(gè)像隊(duì)列一樣的數(shù)組(先進(jìn)先出)
var eventLoop = [ ];
var event;

// “永遠(yuǎn)”執(zhí)行
while (true) {
    // 執(zhí)行一個(gè)"tick"
    if (eventLoop.length > 0) {
        // 在隊(duì)列中取得下一個(gè)事件
        event = eventLoop.shift();

        // 現(xiàn)在執(zhí)行下一個(gè)事件
        try {
            event();
        }
        catch (err) {
            reportError(err);
        }
    }
}

當(dāng)然湾蔓,這只是一個(gè)用來(lái)展示概念的大幅簡(jiǎn)化的假想代碼。但是對(duì)于幫助我們建立更好的理解來(lái)說(shuō)應(yīng)該夠了砌梆。

如你所見(jiàn)默责,有一個(gè)通過(guò)while循環(huán)來(lái)表現(xiàn)的持續(xù)不斷的循環(huán),這個(gè)循環(huán)的每一次迭代稱為一個(gè)“tick”咸包。在每一個(gè)“tick”中桃序,如果隊(duì)列中有一個(gè)事件在等待,它就會(huì)被取出執(zhí)行烂瘫。這些事件就是你的函數(shù)回調(diào)媒熊。

很重要并需要注意的是,setTimeout(..)不會(huì)將你的回調(diào)放在事件輪詢隊(duì)列上坟比。它設(shè)置一個(gè)定時(shí)器芦鳍;當(dāng)這個(gè)定時(shí)器超時(shí)的時(shí)候,環(huán)境才會(huì)把你的回調(diào)放進(jìn)事件輪詢葛账,這樣在某個(gè)未來(lái)的tick中它將會(huì)被取出執(zhí)行柠衅。

如果在那時(shí)事件輪詢隊(duì)列中已經(jīng)有了20個(gè)事件會(huì)怎么樣?你的回調(diào)要等待籍琳。它會(huì)排到隊(duì)列最后——沒(méi)有一般的方法可以插隊(duì)和跳到隊(duì)列的最前方菲宴。這就解釋了為什么setTimeout(..)計(jì)時(shí)器可能不會(huì)完美地按照預(yù)計(jì)時(shí)間觸發(fā)贷祈。你得到一個(gè)保證(粗略地說(shuō)):你的回調(diào)不會(huì)再你指定的時(shí)間間隔之前被觸發(fā),但是可能會(huì)在這個(gè)時(shí)間間隔之后被觸發(fā)裙顽,具體要看事件隊(duì)列的狀態(tài)付燥。

換句話說(shuō),你的程序通常被打斷成許多小的代碼塊兒愈犹,它們一個(gè)接一個(gè)地在事件輪詢隊(duì)列中執(zhí)行键科。而且從技術(shù)上說(shuō),其他與你的程序沒(méi)有直接關(guān)系的事件也可以穿插在隊(duì)列中漩怎。

注意: 我們提到了“直到最近”勋颖,暗示著ES6改變了事件輪詢隊(duì)列在何處被管理的性質(zhì)。這主要是一個(gè)正式的技術(shù)規(guī)范勋锤,ES6現(xiàn)在明確地指出了事件輪詢應(yīng)當(dāng)如何工作饭玲,這意味著它技術(shù)上屬于JS引擎應(yīng)當(dāng)關(guān)心的范疇內(nèi),而不僅僅是 宿主環(huán)境叁执。這么做的一個(gè)主要原因是為了引入ES6的Promises(我們將在第三章討論)茄厘,因?yàn)槿藗冃枰心芰?duì)事件輪詢隊(duì)列的排隊(duì)操作進(jìn)行直接,細(xì)粒度的控制(參見(jiàn)“協(xié)作”一節(jié)中關(guān)于setTimeout(..0)的討論)谈宛。

并行線程

將“異步”與“并行”兩個(gè)詞經(jīng)常被混為一談次哈,但它們實(shí)際上是十分不同的。記住吆录,異步是關(guān)于 現(xiàn)在稍后 之間的間隙窑滞。但并行是關(guān)于可以同時(shí)發(fā)生的事情。

關(guān)于并行計(jì)算最常見(jiàn)的工具就是進(jìn)程與線程恢筝。進(jìn)程和線程獨(dú)立地哀卫,可能同時(shí)地執(zhí)行:在不同的處理器上,甚至在不同的計(jì)算機(jī)上撬槽,而多個(gè)線程可以共享一個(gè)進(jìn)程的內(nèi)存資源此改。

相比之下,一個(gè)事件輪詢將它的工作打碎成一系列任務(wù)并串行地執(zhí)行它們恢氯,不允許并行訪問(wèn)和更改共享的內(nèi)存带斑。并行與“串行”可能以在不同線程上的事件輪詢協(xié)作的形式共存。

并行線程執(zhí)行的穿插勋拟,與異步事件的穿插發(fā)生在完全不同的粒度等級(jí)上:

比如:

function later() {
    answer = answer * 2;
    console.log( "Meaning of life:", answer );
}

雖然later()的整個(gè)內(nèi)容將被當(dāng)做一個(gè)事件輪詢隊(duì)列的實(shí)體勋磕,但當(dāng)考慮到將要執(zhí)行這段代碼的線程時(shí),實(shí)際上也許會(huì)有許多不同的底層操作敢靡。比如挂滓,answer = answer * 2首先需要讀取當(dāng)前answer的值,再把2放在某個(gè)地方啸胧,然后進(jìn)行乘法計(jì)算赶站,最后把結(jié)果存回到answer幔虏。

在一個(gè)單線程環(huán)境中,線程隊(duì)列中的內(nèi)容都是底層操作真的無(wú)關(guān)緊要贝椿,因?yàn)闆](méi)有什么可以打斷線程想括。但如果你有一個(gè)并行系統(tǒng),在同一個(gè)程序中有兩個(gè)不同的線程烙博,你很可能會(huì)得到無(wú)法預(yù)測(cè)的行為:

考慮這段代碼:

var a = 20;

function foo() {
    a = a + 1;
}

function bar() {
    a = a * 2;
}

// ajax(..) 是一個(gè)給定的庫(kù)中的隨意Ajax函數(shù)
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

在JavaScript的單線程行為下瑟蜈,如果foo()bar()之前執(zhí)行,結(jié)果a42渣窜,但如果bar()foo()之前執(zhí)行铺根,結(jié)果a將是41

如果JS事件共享相同的并列執(zhí)行數(shù)據(jù)乔宿,問(wèn)題將會(huì)變得微妙得多位迂。考慮這兩個(gè)假想代碼段详瑞,它們分別描述了運(yùn)行foo()bar()中代碼的線程將要執(zhí)行的任務(wù)掂林,并考慮如果它們?cè)谕耆嗤臅r(shí)刻運(yùn)行會(huì)發(fā)生什么:

線程1(XY是臨時(shí)的內(nèi)存位置):

foo():
    a. 將`a`的值讀取到`X`
    b. 將`1`存入`Y`
    c. 把`X`和`Y`相加,將結(jié)果存入`X`
  d. 將`X`的值存入`a`

線程2(XY是臨時(shí)的內(nèi)存位置):

bar():
  a. 將`a`的值讀取到`X`
    b. 將`2`存入`Y`
    c. 把`X`和`Y`相乘坝橡,將結(jié)果存入`X`
    d. 將`X`的值存入`a`

現(xiàn)在党饮,讓我們假定這兩個(gè)線程在并行執(zhí)行。你可能發(fā)現(xiàn)了問(wèn)題驳庭,對(duì)吧?它們?cè)谂R時(shí)的步驟中使用共享的內(nèi)存位置XY氯窍。

如果步驟像這樣發(fā)生饲常,a的最終結(jié)果什么?

1a  (將`a`的值讀取到`X`   ==> `20`)
2a  (將`a`的值讀取到`X`   ==> `20`)
1b  (將`1`存入`Y`   ==> `1`)
2b  (將`2`存入`Y`   ==> `2`)
1c  (把`X`和`Y`相加狼讨,將結(jié)果存入`X`   ==> `22`)
1d  (將`X`的值存入`a`   ==> `22`)
2c  (把`X`和`Y`相乘贝淤,將結(jié)果存入`X`   ==> `44`)
2d  (將`X`的值存入`a`   ==> `44`)

a中的結(jié)果將是44。那么這種順序呢政供?

1a  (將`a`的值讀取到`X`   ==> `20`)
2a  (將`a`的值讀取到`X`   ==> `20`)
2b  (將`2`存入`Y`   ==> `2`)
1b  (將`1`存入`Y`   ==> `1`)
2c  (把`X`和`Y`相乘播聪,將結(jié)果存入`X`   ==> `20`)
1c  (把`X`和`Y`相加,將結(jié)果存入`X`   ==> `21`)
1d  (將`X`的值存入`a`   ==> `21`)
2d  (將`X`的值存入`a`   ==> `21`)

a中的結(jié)果將是21布隔。

所以离陶,關(guān)于線程的編程十分刁鉆,因?yàn)槿绻悴徊扇√厥獾牟襟E來(lái)防止這樣的干擾/穿插衅檀,你會(huì)得到令人非常詫異的招刨,不確定的行為。這通常讓人頭疼哀军。

JavaScript從不跨線程共享數(shù)據(jù)沉眶,這意味著不必關(guān)心這一層的不確定性打却。但這并不意味著JS總是確定性的。記得前面foo()bar()的相對(duì)順序產(chǎn)生兩個(gè)不同的結(jié)果嗎(4142)谎倔?

注意: 可能還不明顯柳击,但不是所有的不確定性都是壞的。有時(shí)候它無(wú)關(guān)緊要片习,有時(shí)候它是故意的捌肴。我們會(huì)在本章和后續(xù)幾章中看到更多的例子。

運(yùn)行至完成

因?yàn)镴avaScript是單線程的毯侦,foo()(和bar())中的代碼是原子性的哭靖,這意味著一旦foo()開(kāi)始運(yùn)行,它的全部代碼都會(huì)在bar()中的任何代碼可以運(yùn)行之前執(zhí)行完成侈离,反之亦然试幽。這稱為“運(yùn)行至完成”行為。

事實(shí)上卦碾,運(yùn)行至完成的語(yǔ)義會(huì)在foo()bar()中有更多的代碼時(shí)更明顯铺坞,比如:

var a = 1;
var b = 2;

function foo() {
    a++;
    b = b * a;
    a = b + 3;
}

function bar() {
    b--;
    a = 8 + b;
    b = a * 2;
}

// ajax(..) 是某個(gè)包中任意的Ajax函數(shù)
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

因?yàn)?code>foo()不能被bar()打斷,而且bar()不能被foo()打斷洲胖,所以這個(gè)程序根據(jù)哪一個(gè)先執(zhí)行只有兩種可能的結(jié)果——如果線程存在济榨,foo()bar()中的每一個(gè)語(yǔ)句都可能被穿插,可能的結(jié)果數(shù)量將會(huì)極大地增長(zhǎng)!

代碼塊兒1是同步的(現(xiàn)在 發(fā)生)绿映,但代碼塊兒2和3是異步的(稍后 發(fā)生)擒滑,這意味著它們的執(zhí)行將會(huì)被時(shí)間的間隙分開(kāi)。

代碼塊兒1:

var a = 1;
var b = 2;

代碼塊兒2 (foo()):

a++;
b = b * a;
a = b + 3;

代碼塊兒3 (bar()):

b--;
a = 8 + b;
b = a * 2;

代碼塊兒2和3哪一個(gè)都有可能先執(zhí)行叉弦,所以這個(gè)程序有兩個(gè)可能的結(jié)果丐一,正如這里展示的:

結(jié)果1:

var a = 1;
var b = 2;

// foo()
a++;
b = b * a;
a = b + 3;

// bar()
b--;
a = 8 + b;
b = a * 2;

a; // 11
b; // 22

結(jié)果2:

var a = 1;
var b = 2;

// bar()
b--;
a = 8 + b;
b = a * 2;

// foo()
a++;
b = b * a;
a = b + 3;

a; // 183
b; // 180

同一段代碼有兩種結(jié)果仍然意味著不確定性!但是這是在函數(shù)(事件)順序的水平上淹冰,而不是在使用線程時(shí)語(yǔ)句順序的水平上(或者說(shuō)库车,實(shí)際上是表達(dá)式操作的順序上)。換句話說(shuō)樱拴,他比線程更具有 確定性柠衍。

當(dāng)套用到JavaScript行為時(shí),這種函數(shù)順序的不確定性通常稱為“競(jìng)合狀態(tài)”晶乔,因?yàn)?code>foo()和bar()在互相競(jìng)爭(zhēng)看誰(shuí)會(huì)先運(yùn)行珍坊。明確地說(shuō),它是一個(gè)“競(jìng)合狀態(tài)”因?yàn)槟悴荒芸煽康仡A(yù)測(cè)ab將如何產(chǎn)生瘪弓。

注意: 如果在JS中不知怎的有一個(gè)函數(shù)沒(méi)有運(yùn)行至完成的行為垫蛆,我們會(huì)有更多可能的結(jié)果,對(duì)吧?ES6中引入一個(gè)這樣的東西(見(jiàn)第四章“生成器”)袱饭,但現(xiàn)在不要擔(dān)心川无,我們會(huì)回頭討論它。

并發(fā)

讓我們想象一個(gè)網(wǎng)站虑乖,它顯示一個(gè)隨著用戶向下滾動(dòng)而逐步加載的狀態(tài)更新列表(就像社交網(wǎng)絡(luò)的新消息)懦趋。要使這樣的特性正確工作,(至少)需要兩個(gè)分離的“進(jìn)程” 同時(shí) 執(zhí)行(在同一個(gè)時(shí)間跨度內(nèi)疹味,但沒(méi)必要是同一個(gè)時(shí)間點(diǎn))仅叫。

注意: 我們?cè)谶@里使用帶引號(hào)的“進(jìn)程”,因?yàn)樗鼈儾皇怯?jì)算機(jī)科學(xué)意義上的真正的操作系統(tǒng)級(jí)別的進(jìn)程糙捺。它們是虛擬進(jìn)程诫咱,或者說(shuō)任務(wù),表示一組邏輯上關(guān)聯(lián)洪灯,串行順序的操作坎缭。我們將簡(jiǎn)單地使用“進(jìn)程”而非“任務(wù)”,因?yàn)樵谛g(shù)語(yǔ)層面它與我們討論的概念的定義相匹配签钩。

第一個(gè)“進(jìn)程”將響應(yīng)當(dāng)用戶向下滾動(dòng)頁(yè)面時(shí)觸發(fā)的onscroll事件(發(fā)起取得新內(nèi)容的Ajax請(qǐng)求)掏呼。第二個(gè)“進(jìn)程”將接收返回的Ajax應(yīng)答(將內(nèi)容繪制在頁(yè)面上)。

顯然铅檩,如果用戶向下滾動(dòng)的足夠快憎夷,你也許會(huì)看到在第一個(gè)應(yīng)答返回并處理期間,有兩個(gè)或更多的onscroll事件被觸發(fā)昧旨,因此你將使onscroll事件和Ajax應(yīng)答事件迅速觸發(fā)拾给,互相穿插在一起。

并發(fā)是當(dāng)兩個(gè)或多個(gè)“進(jìn)程”在同一時(shí)間段內(nèi)同時(shí)執(zhí)行兔沃,無(wú)論構(gòu)成它們的各個(gè)操作是否 并行地(在同一時(shí)刻不同的處理器或內(nèi)核)發(fā)生鸣戴。你可以認(rèn)為并發(fā)是“進(jìn)程”級(jí)別的(或任務(wù)級(jí)別)的并行機(jī)制,而不是操作級(jí)別的并行機(jī)制(分割進(jìn)程的線程)粘拾。

注意: 并發(fā)還引入了這些“進(jìn)程”間彼此互動(dòng)的概念。我們稍后會(huì)討論它创千。

在一個(gè)給定的時(shí)間跨度內(nèi)(用戶可以滾動(dòng)的那幾秒)缰雇,讓我們將每個(gè)獨(dú)立的“進(jìn)程”作為一系列事件/操作描繪出來(lái):

“線程”1 (onscroll事件):

onscroll, request 1
onscroll, request 2
onscroll, request 3
onscroll, request 4
onscroll, request 5
onscroll, request 6
onscroll, request 7

“線程”2 (Ajax應(yīng)答事件):

response 1
response 2
response 3
response 4
response 5
response 6
response 7

一個(gè)onscroll事件與一個(gè)Ajax應(yīng)答事件很有可能在同一個(gè) 時(shí)刻 都準(zhǔn)備好被處理了。比如我們?cè)谝粋€(gè)時(shí)間線上描繪一下這些事件的話:

onscroll, request 1
onscroll, request 2          response 1
onscroll, request 3          response 2
response 3
onscroll, request 4
onscroll, request 5
onscroll, request 6          response 4
onscroll, request 7
response 6
response 5
response 7

但是追驴,回到本章前面的事件輪詢概念械哟,JS一次只能處理一個(gè)事件,所以不是onscroll, request 2首先發(fā)生就是response 1首先發(fā)生殿雪,但是他們不可能完全在同一時(shí)刻發(fā)生暇咆。就像學(xué)校食堂的孩子們一樣,不管他們?cè)陂T(mén)口擠成什么樣,他們最后都不得不排成一個(gè)隊(duì)來(lái)打飯爸业!

讓我們來(lái)描繪一下所有這些事件在事件輪詢隊(duì)列上穿插的情況:

事件輪詢隊(duì)列:

onscroll, request 1   <--- 進(jìn)程1開(kāi)始
onscroll, request 2
response 1            <--- 進(jìn)程2開(kāi)始
onscroll, request 3
response 2
response 3
onscroll, request 4
onscroll, request 5
onscroll, request 6
response 4
onscroll, request 7   <--- 進(jìn)程1結(jié)束
response 6
response 5
response 7            <--- 進(jìn)程2結(jié)束

“進(jìn)程1”和“進(jìn)程2”并發(fā)地運(yùn)行(任務(wù)級(jí)別的并行)其骄,但是它們的個(gè)別事件在事件輪詢隊(duì)列上順序地運(yùn)行。

順便說(shuō)一句扯旷,注意到response 6response 5沒(méi)有按照預(yù)想的順序應(yīng)答嗎拯爽?

單線程事件輪詢是并發(fā)的一種表達(dá)(當(dāng)然還有其他的表達(dá),我們稍后討論)钧忽。

非互動(dòng)

在同一個(gè)程序中兩個(gè)或更多的“進(jìn)程”在穿插它們的步驟/事件時(shí)毯炮,如果它們的任務(wù)之間沒(méi)有聯(lián)系,那么他們就沒(méi)必要互動(dòng)耸黑。如果它們不互動(dòng)桃煎,不確定性就是完全可以接受的。

舉個(gè)例子:

var res = {};

function foo(results) {
    res.foo = results;
}

function bar(results) {
    res.bar = results;
}

// ajax(..) 是某個(gè)包中任意的Ajax函數(shù)
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

foo()bar()是兩個(gè)并發(fā)的“進(jìn)程”大刊,而且它們被觸發(fā)的順序是不確定的为迈。但對(duì)我們的程序的結(jié)構(gòu)來(lái)講它們的觸發(fā)順序無(wú)關(guān)緊要,因?yàn)樗鼈兊男袨橄嗷オ?dú)立所以不需要互動(dòng)奈揍。

這不是一個(gè)“競(jìng)合狀態(tài)”Bug曲尸,因?yàn)檫@段代碼總能夠正確工作,與順序無(wú)關(guān)男翰。

互動(dòng)

更常見(jiàn)的是另患,通過(guò)作用域和/或DOM,并發(fā)的“進(jìn)程”將有必要間接地互動(dòng)蛾绎。當(dāng)這樣的互動(dòng)將要發(fā)生時(shí)昆箕,你需要協(xié)調(diào)這些互動(dòng)行為來(lái)防止前面講述的“競(jìng)合狀態(tài)”。

這里是兩個(gè)由于隱含的順序而互動(dòng)的并發(fā)“進(jìn)程”的例子租冠,它 有時(shí)會(huì)出錯(cuò)

var res = [];

function response(data) {
    res.push( data );
}

// ajax(..) 是某個(gè)包中任意的Ajax函數(shù)
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

并發(fā)的“進(jìn)程”是那兩個(gè)將要處理Ajax應(yīng)答的response()調(diào)用鹏倘。它們誰(shuí)都有可能先發(fā)生。

假定我們期望的行為是res[0]擁有"http://some.url.1"調(diào)用的結(jié)果顽爹,而res[1]擁有"http://some.url.2"調(diào)用的結(jié)果纤泵。有時(shí)候結(jié)果確實(shí)是這樣,而有時(shí)候則相反镜粤,要看哪一個(gè)調(diào)用首先完成捏题。很有可能,這種不確定性是一個(gè)“競(jìng)合狀態(tài)”Bug肉渴。

注意: 在這些情況下要極其警惕你可能做出的主觀臆測(cè)公荧。比如這樣的情況就沒(méi)什么不尋常:一個(gè)開(kāi)發(fā)者觀察到"http://some.url.2"的應(yīng)答“總是”比"http://some.url.1"要慢得多,也許有賴于它們所做的任務(wù)(比如同规,一個(gè)執(zhí)行數(shù)據(jù)庫(kù)任務(wù)而另一個(gè)只是取得靜態(tài)文件)循狰,所以觀察到的順序看起來(lái)總是所期望的窟社。就算兩個(gè)請(qǐng)求都發(fā)到同一個(gè)服務(wù)器,而且它故意以確定的順序應(yīng)答绪钥,也不能 真正 保證應(yīng)答回到瀏覽器的順序灿里。

所以,為了解決這樣的競(jìng)合狀態(tài)昧识,你可以協(xié)調(diào)互動(dòng)的順序:

var res = [];

function response(data) {
    if (data.url == "http://some.url.1") {
        res[0] = data;
    }
    else if (data.url == "http://some.url.2") {
        res[1] = data;
    }
}

// ajax(..) 是某個(gè)包中任意的Ajax函數(shù)
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

無(wú)論哪個(gè)Ajax應(yīng)答首先返回钠四,我們都考察它的data.url(當(dāng)然,假設(shè)這樣的數(shù)據(jù)會(huì)從服務(wù)器返回)來(lái)找到應(yīng)答數(shù)據(jù)應(yīng)當(dāng)在res數(shù)組中占有的位置跪楞。res[0]將總是持有"http://some.url.1"的結(jié)果缀去,而res[1]將總是持有"http://some.url.2"的結(jié)果。通過(guò)簡(jiǎn)單的協(xié)調(diào)甸祭,我們消除了“競(jìng)合狀態(tài)”的不確定性缕碎。

這個(gè)場(chǎng)景的同樣道理可以適用于這樣的情況:多個(gè)并發(fā)的函數(shù)調(diào)用通過(guò)共享的DOM互動(dòng),比如一個(gè)在更新<div>的內(nèi)容而另一個(gè)在更新<div>的樣式或?qū)傩裕ū热缫坏〥OM元素?fù)碛袃?nèi)容就使它變得可見(jiàn))池户。你可能不想在DOM元素?fù)碛袃?nèi)容之前顯示它咏雌,所以協(xié)調(diào)工作就必須保證正確順序的互動(dòng)。

沒(méi)有協(xié)調(diào)的互動(dòng)校焦,有些并發(fā)的場(chǎng)景 總是出錯(cuò)(不僅僅是 有時(shí))赊抖。考慮下面的代碼:

var a, b;

function foo(x) {
    a = x * 2;
    baz();
}

function bar(y) {
    b = y * 2;
    baz();
}

function baz() {
    console.log(a + b);
}

// ajax(..) 是某個(gè)包中任意的Ajax函數(shù)
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

在這個(gè)例子中寨典,不管foo()bar()誰(shuí)先觸發(fā)氛雪,總是會(huì)使baz()運(yùn)行的太早了(ab之一還是空的時(shí)候),但是第二個(gè)baz()調(diào)用將可以工作耸成,因?yàn)?code>a和b將都是可用的报亩。

有許多不同的方法可以解決這個(gè)狀態(tài)。這是簡(jiǎn)單的一種:

var a, b;

function foo(x) {
    a = x * 2;
    if (a && b) {
        baz();
    }
}

function bar(y) {
    b = y * 2;
    if (a && b) {
        baz();
    }
}

function baz() {
    console.log( a + b );
}

// ajax(..) 是某個(gè)包中任意的Ajax函數(shù)
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

baz()調(diào)用周?chē)?code>if (a && b)條件通常稱為“大門(mén)”井氢,因?yàn)槲覀儾荒艽_定ab到來(lái)的順序弦追,但在打開(kāi)大門(mén)(調(diào)用baz())之前我們等待它們?nèi)康竭_(dá)。

另一種你可能會(huì)遇到的并發(fā)互動(dòng)狀態(tài)有時(shí)稱為“競(jìng)爭(zhēng)”花竞,單更準(zhǔn)確地說(shuō)應(yīng)該叫“門(mén)閂”劲件。它的行為特點(diǎn)是“先到者勝”。在這里不確定性是可以接受的约急,因?yàn)槟忝鞔_指出“競(jìng)爭(zhēng)”的終點(diǎn)線上只有一個(gè)勝利者寇仓。

考慮這段有問(wèn)題的代碼:

var a;

function foo(x) {
    a = x * 2;
    baz();
}

function bar(x) {
    a = x / 2;
    baz();
}

function baz() {
    console.log( a );
}

// ajax(..) 是某個(gè)包中任意的Ajax函數(shù)
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

不管哪一個(gè)函數(shù)最后觸發(fā)(foo()bar()),它不僅會(huì)覆蓋前一個(gè)函數(shù)對(duì)a的賦值烤宙,還會(huì)重復(fù)調(diào)用baz()(不太可能是期望的)。

所以俭嘁,我們可以用一個(gè)簡(jiǎn)單的門(mén)閂來(lái)協(xié)調(diào)互動(dòng)躺枕,僅讓第一個(gè)過(guò)去:

var a;

function foo(x) {
    if (a == undefined) {
        a = x * 2;
        baz();
    }
}

function bar(x) {
    if (a == undefined) {
        a = x / 2;
        baz();
    }
}

function baz() {
    console.log( a );
}

// ajax(..) 是某個(gè)包中任意的Ajax函數(shù)
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

if (a == undefined)條件僅會(huì)讓foo()bar()中的第一個(gè)通過(guò),而第二個(gè)(以及后續(xù)所有的)調(diào)用將會(huì)被忽略。第二名什么也得不到拐云!

注意: 在所有這些場(chǎng)景中罢猪,為了簡(jiǎn)化說(shuō)明的目的我們都用了全局變量,這里我們沒(méi)有任何理由需要這么做叉瘩。只要我們討論中的函數(shù)可以訪問(wèn)變量(通過(guò)作用域)膳帕,它們就可以正常工作。依賴于詞法作用域變量(參見(jiàn)本叢書(shū)的 作用域與閉包 )薇缅,和這些例子中實(shí)質(zhì)上的全局變量危彩,是這種并發(fā)協(xié)調(diào)形式的一個(gè)明顯的缺點(diǎn)。在以后的幾章中泳桦,我們會(huì)看到其他的在這方面干凈得多的協(xié)調(diào)方法汤徽。

協(xié)作

另一種并發(fā)協(xié)調(diào)的表達(dá)稱為“協(xié)作并發(fā)”,它并不那么看重在作用域中通過(guò)共享值互動(dòng)(雖然這依然是允許的>淖)谒府。它的目標(biāo)是將一個(gè)長(zhǎng)時(shí)間運(yùn)行的“進(jìn)程”打斷為許多步驟或批處理,以至于其他的并發(fā)“進(jìn)程”有機(jī)會(huì)將它們的操作穿插進(jìn)事件輪詢隊(duì)列浮毯。

舉個(gè)例子完疫,考慮一個(gè)Ajax應(yīng)答處理器,它需要遍歷一個(gè)很長(zhǎng)的結(jié)果列表來(lái)將值變形债蓝。我們將使用Array#map(..)來(lái)讓代碼短一些:

var res = [];

// `response(..)`從Ajax調(diào)用收到一個(gè)結(jié)果數(shù)組
function response(data) {
    // 連接到既存的`res`數(shù)組上
    res = res.concat(
        // 制造一個(gè)新的變形過(guò)的數(shù)組壳鹤,所有的`data`值都翻倍
        data.map( function(val){
            return val * 2;
        } )
    );
}

// ajax(..) 是某個(gè)包中任意的Ajax函數(shù)
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

如果"http://some.url.1"首先返回它的結(jié)果,整個(gè)結(jié)果列表將會(huì)一次性映射進(jìn)res惦蚊。如果只有幾千或更少的結(jié)果記錄器虾,一般來(lái)說(shuō)不是什么大事。但假如有1千萬(wàn)個(gè)記錄蹦锋,那么就可能會(huì)花一段時(shí)間運(yùn)行(在強(qiáng)大的筆記本電腦上花幾秒鐘兆沙,在移動(dòng)設(shè)備上花的時(shí)間長(zhǎng)得多,等等)莉掂。

當(dāng)這樣的“處理”運(yùn)行時(shí)葛圃,頁(yè)面上沒(méi)有任何事情可以發(fā)生,包括不能有另一個(gè)response(..)調(diào)用憎妙,不能有UI更新库正,甚至不能有用戶事件比如滾動(dòng),打字厘唾,按鈕點(diǎn)擊等褥符。非常痛苦。

所以抚垃,為了制造協(xié)作性更強(qiáng)喷楣、更友好而且不獨(dú)占事件輪詢隊(duì)列的并發(fā)系統(tǒng)趟大,你可以在一個(gè)異步批處理中處理這些結(jié)果,在批處理的每一步都“讓出”事件輪詢來(lái)讓其他等待的事件發(fā)生铣焊。

這是一個(gè)非常簡(jiǎn)單的方法:

var res = [];

// `response(..)`從Ajax調(diào)用收到一個(gè)結(jié)果數(shù)組
function response(data) {
    // 我們一次只處理1000件
    var chunk = data.splice( 0, 1000 );

    // 連接到既存的`res`數(shù)組上
    res = res.concat(
        // 制造一個(gè)新的變形過(guò)的數(shù)組逊朽,所有的`data`值都翻倍
        chunk.map( function(val){
            return val * 2;
        } )
    );

    // 還有東西要處理嗎?
    if (data.length > 0) {
        // 異步規(guī)劃下一個(gè)批處理
        setTimeout( function(){
            response( data );
        }, 0 );
    }
}

// ajax(..) 是某個(gè)包中任意的Ajax函數(shù)
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

我們以每次最大1000件作為一個(gè)塊兒處理數(shù)據(jù)曲伊。這樣叽讳,我們保證每個(gè)“進(jìn)程”都是短時(shí)間運(yùn)行的,即便這意味著會(huì)有許多后續(xù)的“進(jìn)程”坟募,在事件輪詢隊(duì)列上的穿插將會(huì)給我們一個(gè)響應(yīng)性(性能)強(qiáng)得多的網(wǎng)站/應(yīng)用程序岛蚤。

當(dāng)然,我們沒(méi)有對(duì)任何這些“進(jìn)程”的順序進(jìn)行互動(dòng)協(xié)調(diào)婿屹,所以在res中的結(jié)果的順序是不可預(yù)知的灭美。如果要求順序,你需要使用我們之前討論的互動(dòng)技術(shù)昂利,或者在本書(shū)后續(xù)章節(jié)中介紹的其他技術(shù)届腐。

我們使用setTimeout(..0)(黑科技)來(lái)異步排程,基本上它的意思是“將這個(gè)函數(shù)貼在事件輪詢隊(duì)列的末尾”蜂奸。

注意: 從技術(shù)上講犁苏,setTimeout(..0)沒(méi)有直接將一條記錄插入事件輪詢隊(duì)列。計(jì)時(shí)器將會(huì)在下一個(gè)運(yùn)行機(jī)會(huì)將事件插入扩所。比如果漾,兩個(gè)連續(xù)的setTimeout(..0)調(diào)用不會(huì)嚴(yán)格保證以調(diào)用的順序被處理猛遍,所以我們可能看到各種時(shí)間偏移的情況剔应,使這樣的事件的順序是不可預(yù)知的效斑。在Node.js中,一個(gè)相似的方式是process.nextTick(..)袁勺。不管那將會(huì)有多方便(而且通常性能更好)雹食,(還)沒(méi)有一個(gè)直接的方法可以橫跨所有環(huán)境來(lái)保證異步事件順序。我們會(huì)在下一節(jié)詳細(xì)討論這個(gè)話題期丰。

Jobs

在ES6中群叶,在事件輪詢隊(duì)列之上引入了一層新概念,稱為“工作隊(duì)列(Job queue)”钝荡。你最有可能接觸它的地方是在Promises(見(jiàn)第三章)的異步行為中街立。

不幸的是,它目前是一個(gè)沒(méi)有公開(kāi)API的機(jī)制埠通,因此要演示它有些兜圈子赎离。我們不得不僅僅在概念上描述它,這樣當(dāng)我們?cè)诘谌轮杏懻摦惒叫袨闀r(shí)端辱,你將會(huì)理解那些動(dòng)作行為是如何排程與處理的梁剔。

那么圾浅,我能找到的考慮它的最佳方式是:“工作隊(duì)列”是一個(gè)掛靠在事件輪詢隊(duì)列的每個(gè)tick末尾的隊(duì)列。在事件輪詢的一個(gè)tick期間內(nèi)憾朴,某些可能發(fā)生的隱含異步動(dòng)作的行為將不會(huì)導(dǎo)致一個(gè)全新的事件加入事件輪詢隊(duì)列,而是在當(dāng)前tick的工作隊(duì)列的末尾加入一個(gè)新的記錄(也就是一個(gè)Job)喷鸽。

它好像是在說(shuō)众雷,“哦,另一件需要我 稍后 去做的事兒做祝,但是保證它在其他任何事情發(fā)生之間發(fā)生砾省。”

或者混槐,用一個(gè)比喻:事件輪詢隊(duì)列就像一個(gè)游樂(lè)園項(xiàng)目编兄,一旦你乘坐完一次,你就不得不去隊(duì)尾排隊(duì)來(lái)乘坐下一次声登。而工作隊(duì)列就像乘坐完后狠鸳,立即插隊(duì)乘坐下一次。

一個(gè)Job還可能會(huì)導(dǎo)致更多的Job被加入同一個(gè)隊(duì)列的末尾悯嗓。所以件舵,一個(gè)在理論上可能的情況是,Job“輪詢”(一個(gè)Job持續(xù)不斷地加入其他Job等)會(huì)無(wú)限地轉(zhuǎn)下去脯厨,從而拖住程序不能移動(dòng)到一下一個(gè)事件輪詢tick铅祸。這與在你的代碼中表達(dá)一個(gè)長(zhǎng)時(shí)間運(yùn)行或無(wú)限循環(huán)(比如while (true) ..)在概念上幾乎是一樣的。

Job的精神有點(diǎn)兒像setTimeout(..0)黑科技合武,但以一種定義明確得多的方式實(shí)現(xiàn)临梗,而且保證順序: 稍后,但盡快稼跳。

讓我們想象一個(gè)用于Job排程的API盟庞,并叫它schedule(..)∑穹罚考慮如下代碼:

console.log( "A" );

setTimeout( function(){
    console.log( "B" );
}, 0 );

// 理論上的 "Job API"
schedule( function(){
    console.log( "C" );

    schedule( function(){
        console.log( "D" );
    } );
} );

你肯能會(huì)期望它打印出A B C D茫经,但是它將會(huì)打出A C D B,因?yàn)镴ob發(fā)生在當(dāng)前的事件輪詢tick的末尾萎津,而定時(shí)器會(huì)在 下一個(gè) 事件輪詢tick(如果可用的話P渡 )觸發(fā)排程。

在第三章中锉屈,我們會(huì)看到Promises的異步行為是基于Job的荤傲,所以搞明白它與事件輪詢行為的聯(lián)系是很重要的。

語(yǔ)句排序

我們?cè)诖a中表達(dá)語(yǔ)句的順序沒(méi)有必要與JS引擎執(zhí)行它們的順序相同颈渊。這可能看起來(lái)像是個(gè)奇怪的論斷遂黍,所以我們簡(jiǎn)單地探索一下终佛。

但在我們開(kāi)始之前,我們應(yīng)當(dāng)對(duì)一些事情十分清楚:從程序的角度看雾家,語(yǔ)言的規(guī)則/文法(參見(jiàn)本叢書(shū)的 類型與文法)為語(yǔ)句的順序決定了一個(gè)非沉逭茫可預(yù)知、可靠的行為芯咧。所以我們將要討論的是在你的JS程序中 應(yīng)當(dāng)永遠(yuǎn)觀察不到的東西牙捉。

警告: 如果你曾經(jīng) 觀察到 過(guò)我們將要描述的編譯器語(yǔ)句重排,那明顯是違反了語(yǔ)言規(guī)范敬飒,而且無(wú)疑是那個(gè)JS引擎的Bug——它應(yīng)當(dāng)被報(bào)告并且修復(fù)邪铲!但是更常見(jiàn)的是你 懷疑 JS引擎里發(fā)生了什么瘋狂的事,而事實(shí)上它只是你自己代碼中的一個(gè)Bug(可能是一個(gè)“競(jìng)合狀態(tài)”)——所以先檢查那里无拗,多檢查幾遍带到。在JS調(diào)試器使用斷點(diǎn)并一行一行地步過(guò)你的代碼,將是幫你在 你的代碼 中找出這樣的Bug的最強(qiáng)大的工具英染。

考慮下面的代碼:

var a, b;

a = 10;
b = 30;

a = a + 1;
b = b + 1;

console.log( a + b ); // 42

這段代碼沒(méi)有任何異步表達(dá)(除了早先討論的罕見(jiàn)的console異步I/O)揽惹,所以最有可能的推測(cè)是它會(huì)一行一行地、從上到下地處理税迷。

但是永丝,JS引擎 有可能,在編譯完這段代碼后(是的箭养,JS是被編譯的——見(jiàn)本叢書(shū)的 作用域與閉包)發(fā)現(xiàn)有機(jī)會(huì)通過(guò)(安全地)重新安排這些語(yǔ)句的順序來(lái)使你的代碼運(yùn)行得更快慕嚷。實(shí)質(zhì)上,只要你觀察不到重排毕泌,一切都是合理的喝检。

舉個(gè)例子,引擎可能會(huì)發(fā)現(xiàn)如果實(shí)際上這樣執(zhí)行代碼會(huì)更快:

var a, b;

a = 10;
a++;

b = 30;
b++;

console.log( a + b ); // 42

或者是這樣:

var a, b;

a = 11;
b = 31;

console.log( a + b ); // 42

或者甚至是:

// 因?yàn)閌a`和`b`都不再被使用撼泛,我們可以內(nèi)聯(lián)而且根本不需要它們!
console.log( 42 ); // 42

在所有這些情況下挠说,JS引擎在它的編譯期間進(jìn)行著安全的優(yōu)化,而最終的 可觀察到 的結(jié)果將是相同的愿题。

但也有一個(gè)場(chǎng)景损俭,這些特殊的優(yōu)化是不安全的,因而也是不被允許的(當(dāng)然潘酗,不是說(shuō)它一點(diǎn)兒都沒(méi)優(yōu)化):

var a, b;

a = 10;
b = 30;

// 我們需要`a`和`b`遞增之前的狀態(tài)杆兵!
console.log( a * b ); // 300

a = a + 1;
b = b + 1;

console.log( a + b ); // 42

編譯器重排會(huì)造成可觀測(cè)的副作用(因此絕不會(huì)被允許)的其他例子,包括任何帶有副作用的函數(shù)調(diào)用(特別是getter函數(shù))仔夺,或者ES6的Proxy對(duì)象(參見(jiàn)本叢書(shū)的 ES6與未來(lái))琐脏。

考慮如下代碼:

function foo() {
    console.log( b );
    return 1;
}

var a, b, c;

// ES5.1 getter 字面語(yǔ)法
c = {
    get bar() {
        console.log( a );
        return 1;
    }
};

a = 10;
b = 30;

a += foo();             // 30
b += c.bar;             // 11

console.log( a + b );   // 42

如果不是為了這個(gè)代碼段中的console.log(..)語(yǔ)句(只是作為這個(gè)例子中觀察副作用的方便形式),JS引擎將會(huì)更加自由,如果它想(誰(shuí)知道它想不想H杖埂吹艇?),它會(huì)重排這段代碼:

// ...

a = 10 + foo();
b = 30 + c.bar;

// ...

多虧JS語(yǔ)義昂拂,我們不會(huì)觀測(cè)到看起來(lái)很危險(xiǎn)的編譯器語(yǔ)句重排受神,但是理解源代碼被編寫(xiě)的方式(從上到下)與它在編譯后運(yùn)行的方式之間的聯(lián)系是多么微弱,依然是很重要的格侯。

編譯器語(yǔ)句重排幾乎是并發(fā)與互動(dòng)的微型比喻路克。作為一個(gè)一般概念,這樣的意識(shí)可以幫你更好地理解異步JS代碼流問(wèn)題养交。

復(fù)習(xí)

一個(gè)JavaScript程序總是被打斷為兩個(gè)或更多的代碼塊兒,第一個(gè)代碼塊兒 現(xiàn)在 運(yùn)行瓢宦,下一個(gè)代碼塊兒 稍后 運(yùn)行碎连,來(lái)響應(yīng)一個(gè)事件。雖然程序是一塊兒一塊兒地被執(zhí)行的驮履,但它們都共享相同的程序作用域和狀態(tài)鱼辙,所以對(duì)狀態(tài)的每次修改都是在前一個(gè)狀態(tài)之上的。

不論何時(shí)有事件要運(yùn)行玫镐,事件輪詢 將運(yùn)行至隊(duì)列為空倒戏。事件輪詢的每次迭代稱為一個(gè)“tick”。用戶交互恐似,IO杜跷,和定時(shí)器會(huì)將事件在事件隊(duì)列中排隊(duì)。

在任意給定的時(shí)刻矫夷,一次只有一個(gè)隊(duì)列中的事件可以被處理葛闷。當(dāng)事件執(zhí)行時(shí),他可以直接或間接地導(dǎo)致一個(gè)或更多的后續(xù)事件双藕。

并發(fā)是當(dāng)兩個(gè)或多個(gè)事件鏈條隨著事件相互穿插淑趾,因此從高層的角度來(lái)看,它們?cè)?同時(shí) 運(yùn)行(即便在給定的某一時(shí)刻只有一個(gè)事件在被處理)忧陪。

在這些并發(fā)“進(jìn)程”之間進(jìn)行某種形式的互動(dòng)協(xié)調(diào)通常是有必要的扣泊,比如保證順序或防止“競(jìng)合狀態(tài)”。這些“進(jìn)程”還可以 協(xié)作:通過(guò)將它們自己打斷為小的代碼塊兒來(lái)允許其他“進(jìn)程”穿插嘶摊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末延蟹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子更卒,更是在濱河造成了極大的恐慌等孵,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹂空,死亡現(xiàn)場(chǎng)離奇詭異俯萌,居然都是意外死亡果录,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén)咐熙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)弱恒,“玉大人,你說(shuō)我怎么就攤上這事棋恼》档” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵爪飘,是天一觀的道長(zhǎng)义起。 經(jīng)常有香客問(wèn)我,道長(zhǎng)师崎,這世上最難降的妖魔是什么默终? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮犁罩,結(jié)果婚禮上齐蔽,老公的妹妹穿的比我還像新娘。我一直安慰自己床估,他們只是感情好含滴,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著丐巫,像睡著了一般谈况。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上递胧,一...
    開(kāi)封第一講書(shū)人閱讀 52,184評(píng)論 1 308
  • 那天鸦做,我揣著相機(jī)與錄音,去河邊找鬼谓着。 笑死泼诱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赊锚。 我是一名探鬼主播治筒,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼舷蒲!你這毒婦竟也來(lái)了耸袜?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤牲平,失蹤者是張志新(化名)和其女友劉穎堤框,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜈抓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年启绰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沟使。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡委可,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出腊嗡,到底是詐尸還是另有隱情着倾,我是刑警寧澤,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布燕少,位于F島的核電站卡者,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏客们。R本人自食惡果不足惜虎眨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望镶摘。 院中可真熱鬧,春花似錦岳守、人聲如沸凄敢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)涝缝。三九已至,卻和暖如春譬重,著一層夾襖步出監(jiān)牢的瞬間拒逮,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工臀规, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留滩援,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓塔嬉,卻偏偏與公主長(zhǎng)得像玩徊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子谨究,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容