1.單線程模型
單線程模型指的是驹暑,JavaScript 只在一個線程上運行拨脉。也就是說或衡,JavaScript 同時只能執(zhí)行一個任務(wù)焦影,其他任務(wù)都必須在后面排隊等待。
注意封断,JavaScript 只在一個線程上運行斯辰,不代表 JavaScript 引擎只有一個線程。事實上坡疼,JavaScript 引擎有多個線程彬呻,單個腳本只能在一個線程上運行(稱為主線程),其他線程都是在后臺配合。
JavaScript 之所以采用單線程闸氮,而不是多線程剪况,跟歷史有關(guān)系。JavaScript 從誕生起就是單線程蒲跨,原因是不想讓瀏覽器變得太復雜译断,因為多線程需要共享資源、且有可能修改彼此的運行結(jié)果或悲,對于一種網(wǎng)頁腳本語言來說孙咪,這就太復雜了。如果 JavaScript 同時有兩個線程巡语,一個線程在網(wǎng)頁 DOM 節(jié)點上添加內(nèi)容翎蹈,另一個線程刪除了這個節(jié)點,這時瀏覽器應該以哪個線程為準男公?是不是還要有鎖機制荤堪?所以,為了避免復雜性枢赔,JavaScript 一開始就是單線程澄阳,這已經(jīng)成了這門語言的核心特征,將來也不會改變糠爬。
這種模式的好處是實現(xiàn)起來比較簡單寇荧,執(zhí)行環(huán)境相對單純举庶;壞處是只要有一個任務(wù)耗時很長执隧,后面的任務(wù)都必須排隊等著,會拖延整個程序的執(zhí)行户侥。常見的瀏覽器無響應(假死)镀琉,往往就是因為某一段 JavaScript 代碼長時間運行(比如死循環(huán)),導致整個頁面卡在這個地方蕊唐,其他任務(wù)無法執(zhí)行屋摔。JavaScript 語言本身并不慢,慢的是讀寫外部數(shù)據(jù)替梨,比如等待 Ajax 請求返回結(jié)果钓试。這個時候,如果對方服務(wù)器遲遲沒有響應副瀑,或者網(wǎng)絡(luò)不通暢弓熏,就會導致腳本的長時間停滯。
如果排隊是因為計算量大糠睡,CPU 忙不過來挽鞠,倒也算了,但是很多時候 CPU 是閑著的,因為 IO 操作(輸入輸出)很慢(比如 Ajax 操作從網(wǎng)絡(luò)讀取數(shù)據(jù))信认,不得不等著結(jié)果出來材义,再往下執(zhí)行。JavaScript 語言的設(shè)計者意識到嫁赏,這時 CPU 完全可以不管 IO 操作其掂,掛起處于等待中的任務(wù),先運行排在后面的任務(wù)橄教。等到 IO 操作返回了結(jié)果清寇,再回過頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去护蝶。這種機制就是 JavaScript 內(nèi)部采用的“事件循環(huán)”機制(Event Loop)华烟。
單線程模型雖然對 JavaScript 構(gòu)成了很大的限制,但也因此使它具備了其他語言不具備的優(yōu)勢持灰。如果用得好盔夜,JavaScript 程序是不會出現(xiàn)堵塞的,這就是為什么 Node 可以用很少的資源堤魁,應付大流量訪問的原因喂链。
為了利用多核 CPU 的計算能力,HTML5 提出 Web Worker 標準妥泉,允許 JavaScript 腳本創(chuàng)建多個線程椭微,但是子線程完全受主線程控制,且不得操作 DOM盲链。所以蝇率,這個新標準并沒有改變 JavaScript 單線程的本質(zhì)。
2.同步任務(wù)和異步任務(wù)
程序里面所有的任務(wù)刽沾,可以分成兩類:同步任務(wù)(synchronous)和異步任務(wù)(asynchronous)本慕。
同步任務(wù)是那些沒有被引擎掛起、在主線程上排隊執(zhí)行的任務(wù)侧漓。只有前一個任務(wù)執(zhí)行完畢锅尘,才能執(zhí)行后一個任務(wù)。
異步任務(wù)是那些被引擎放在一邊布蔗,不進入主線程藤违、而進入任務(wù)隊列的任務(wù)。只有引擎認為某個異步任務(wù)可以執(zhí)行了(比如 Ajax 操作從服務(wù)器得到了結(jié)果)纵揍,該任務(wù)(采用回調(diào)函數(shù)的形式)才會進入主線程執(zhí)行顿乒。排在異步任務(wù)后面的代碼,不用等待異步任務(wù)結(jié)束會馬上運行骡男,也就是說淆游,異步任務(wù)不具有“堵塞”效應傍睹。
舉例來說,Ajax 操作可以當作同步任務(wù)處理犹菱,也可以當作異步任務(wù)處理拾稳,由開發(fā)者決定。如果是同步任務(wù)腊脱,主線程就等著 Ajax 操作返回結(jié)果访得,再往下執(zhí)行;如果是異步任務(wù)陕凹,主線程在發(fā)出 Ajax 請求以后悍抑,就直接往下執(zhí)行,等到 Ajax 操作有了結(jié)果杜耙,主線程再執(zhí)行對應的回調(diào)函數(shù)搜骡。
3.任務(wù)隊列和事件循環(huán)
JavaScript 運行時,除了一個正在運行的主線程佑女,引擎還提供一個任務(wù)隊列(task queue)记靡,里面是各種需要當前程序處理的異步任務(wù)。(實際上团驱,根據(jù)異步任務(wù)的類型摸吠,存在多個任務(wù)隊列。為了方便理解嚎花,這里假設(shè)只存在一個隊列寸痢。)
首先,主線程會去執(zhí)行所有的同步任務(wù)紊选。等到同步任務(wù)全部執(zhí)行完啼止,就會去看任務(wù)隊列里面的異步任務(wù)。如果滿足條件丛楚,那么異步任務(wù)就重新進入主線程開始執(zhí)行族壳,這時它就變成同步任務(wù)了憔辫。等到執(zhí)行完趣些,下一個異步任務(wù)再進入主線程開始執(zhí)行。一旦任務(wù)隊列清空贰您,程序就結(jié)束執(zhí)行坏平。
異步任務(wù)的寫法通常是回調(diào)函數(shù)。一旦異步任務(wù)重新進入主線程锦亦,就會執(zhí)行對應的回調(diào)函數(shù)舶替。如果一個異步任務(wù)沒有回調(diào)函數(shù),就不會進入任務(wù)隊列杠园,也就是說顾瞪,不會重新進入主線程,因為沒有用回調(diào)函數(shù)指定下一步的操作。
JavaScript 引擎怎么知道異步任務(wù)有沒有結(jié)果陈醒,能不能進入主線程呢惕橙?答案就是引擎在不停地檢查,一遍又一遍钉跷,只要同步任務(wù)執(zhí)行完了弥鹦,引擎就會去檢查那些掛起來的異步任務(wù),是不是可以進入主線程了爷辙。這種循環(huán)檢查的機制彬坏,就叫做事件循環(huán)(Event Loop)。維基百科的定義是:“事件循環(huán)是一個程序結(jié)構(gòu)膝晾,用于等待和發(fā)送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”栓始。
來源:JavaScript 標準參考教程