一、單線程和任務隊列
- 單線程就意味著夸溶,所有任務需要排隊逸吵,前一個任務結(jié)束,才會執(zhí)行后一個任務缝裁。如果前一個任務耗時很長扫皱,后一個任務就不得不一直等待。
- 如果排隊是因為計算量過大捷绑,CPU忙不過來韩脑,倒也算了,但是很多時候CPU是閑著的粹污,因為IO設備(輸入輸出設備)很慢(比如Ajax操作從網(wǎng)絡讀取數(shù)據(jù))段多,不得不等著結(jié)果出來,再往下執(zhí)行!
- JavaScript語言的設計者意識到壮吩,這時主線程完全可以不管IO設備进苍,掛起處于等待中的任務加缘,先運行排在后邊的任務,等到IO設備返回了結(jié)果觉啊,再回過頭把掛起的任務繼續(xù)執(zhí)行下去拣宏。
- 于是,所有的任務可以分為兩種杠人,一種是同步任務(synchronous)勋乾,另外一種是異步任務(asynchronous)。同步任務指的是嗡善,在主線程上辑莫,排隊執(zhí)行的任務,只有前一個任務執(zhí)行完畢滤奈,才能執(zhí)行后一個任務;異步任務指的是摆昧,不進入主線程,而進入“任務隊列”(task queue)的任務蜒程,只有“任務隊列”通知主線程绅你,某個異步任務可以執(zhí)行了,該任務才會進入主線程執(zhí)行昭躺。
具體來說忌锯,異步執(zhí)行的運行機制如下(同步執(zhí)行也是如此,因為它可以被視為沒有異步任務的異步執(zhí)行)
1. 所有同步任務都在主線程上執(zhí)行领炫,形成一個執(zhí)行棧(execution context stack)偶垮。
2. 主線程之外,還存在一個“任務隊列”(task queue)帝洪,只要異步
任務有了運行結(jié)果似舵,就在“任務隊列”中放置一個事件。
3. 一旦“執(zhí)行棿邢浚”中的所有同步任務執(zhí)行完畢砚哗,系統(tǒng)就會讀取“任務隊
列”,看看里邊有哪些事件砰奕。哪些對應的異步任務蛛芥,于是結(jié)束等待
狀態(tài),進入“執(zhí)行椌”開始執(zhí)行仅淑。
4. 主線程不斷重復上邊的第三步。
下邊就是主線程和任務隊列的示意圖:
主要主線程空了胸哥,就會去讀取“任務隊列”涯竟,這就是JavaScript的運行機制,這個過程會不斷重復。
二昆禽、事件和回調(diào)函數(shù)
- “任務隊列”是一個事件的隊列(也可以理解成消息的隊列)蝗蛙,IO設備完成一項任務,就在“任務隊列”中添加一個事件醉鳖,表示相關(guān)的異步任務可以進入“執(zhí)行椉窆瑁”了,主線程讀取“任務隊列”盗棵,即使讀取里邊有哪些事件壮韭。
- “任務隊列”里邊的事件,除了IO設備的事件以外纹因,還包括一些用戶產(chǎn)生的事件(比如鼠標點擊喷屋,頁面滾動等等)。只要指定過回調(diào)函數(shù)瞭恰,這些事件發(fā)生時屯曹,就會進入任務隊列,等待主線程讀取惊畏。
- 所謂“回調(diào)函數(shù)”(callback)恶耽,就是那些會被主線程掛起來的代碼。異步任務必須執(zhí)行回調(diào)函數(shù)颜启,當主線程開始執(zhí)行異步任務偷俭,就是執(zhí)行對應的回調(diào)函數(shù)。
- “任務隊列”是一個先進先出的數(shù)據(jù)結(jié)構(gòu)缰盏,排在前邊的事件涌萤,優(yōu)先被主線程讀取。主線程的讀取過程口猜,基本上市自動的负溪,只要“執(zhí)行棧”一清空济炎,“任務隊列”上第一位的事件笙以,就會自動進入主線程。但是由于存在后文存提到的“定時器”功能冻辩,主線程首先要檢查一下執(zhí)行時間,某些事件拆祈,只有到了規(guī)定時間恨闪,才能返回主線程。
三放坏、Event Loop
- 主線程從“任務隊列”中讀取事件咙咽,這個過程是循環(huán)不斷的,所以整個的這種運行機制淤年,又稱為Event Loop (事件循環(huán))钧敞。
為了更好地理解Event Loop蜡豹,請看下圖(轉(zhuǎn)引自Philip Roberts的演講《Help, I'm stuck in an event-loop》)。
上圖中溉苛,主線程運行的時候镜廉,產(chǎn)生堆(heap)和棧(stack),棧中的代碼調(diào)用各種外部API愚战,它們在任務隊列中加入各種事件(click娇唯,load,done)寂玲。只要棧中的代碼執(zhí)行完畢塔插,主線程就會去讀取“任務隊列”,依次執(zhí)行那些事件拓哟,所對應的回調(diào)函數(shù)想许。
執(zhí)行棧中的代碼(同步任務),總是在讀取“任務隊列”(異步任務)之前執(zhí)行断序,請看下邊的例子:
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();
上面代碼中的req.send方法是Ajax操作向服務器發(fā)送數(shù)據(jù)流纹,它是一個異步任務,意味著只有當前腳本的所有代碼執(zhí)行完逢倍,系統(tǒng)才會去讀取"任務隊列"捧颅。所以,它與下面的寫法等價较雕。
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};
也就是說碉哑,指定回調(diào)函數(shù)的部分(onload和onerror),在send()方法的前面或后面無關(guān)緊要亮蒋,因為它們屬于執(zhí)行棧的一部分扣典,系統(tǒng)總是執(zhí)行完它們,才會去讀取"任務隊列"慎玖。
四贮尖、定時器
- 除了放置異步任務的事件,“任務隊列”還可以放置定時事件趁怔,即指定某些代碼湿硝,在多少時間后執(zhí)行。這叫做定時器功能(timer),也就是定時執(zhí)行的代碼润努。
- 定時器功能主要由 setTimeout()和setInterval()這兩個函數(shù)來完成关斜,它們的內(nèi)部運行機制完全一樣,區(qū)別在于前者指定的代碼是一次性執(zhí)行铺浇,后者則為反復執(zhí)行痢畜,以下主要討論setTimeout()。
setTimeout()接受兩個參數(shù),第一個是回調(diào)函數(shù)丁稀,第二個是推遲執(zhí)行的毫秒數(shù)吼拥。
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
上面代碼的執(zhí)行結(jié)果是1,3线衫,2凿可,因為setTimeout()將第二行推遲到1000毫秒之后執(zhí)行。
如果將setTimeout()的第二個參數(shù)設為0桶雀,就表示當前代碼執(zhí)行完(執(zhí)行棧清空)以后矿酵,立即執(zhí)行(0毫秒間隔)指定的回調(diào)函數(shù)。
setTimeout(function(){console.log(1);}, 0);
console.log(2);
上面代碼的執(zhí)行結(jié)果總是2矗积,1全肮,因為只有在執(zhí)行完第二行以后,系統(tǒng)才會去執(zhí)行"任務隊列"中的回調(diào)函數(shù)棘捣。
- 總之辜腺,setTimeout(fn,0)的含義是,指定某個任務乍恐,在主線程最早可得的空閑時間執(zhí)行评疗,也就是說,盡可能早的執(zhí)行茵烈。它在“任務隊列”的尾部添加一個事件百匆,因此要等到同步任務和“任務隊列”中的現(xiàn)有事件,都處理完呜投,才會得到執(zhí)行加匈。
- HTML5標準規(guī)定了setTimeout()的第二個參數(shù)的最小值(最短間隔),不得低于4毫秒仑荐,如果低于這個值雕拼,就會自動增加。在此之前粘招,老版本的瀏覽器都將最短間隔設為10毫秒啥寇。另外,對于那些DOM的變動(尤其是涉及頁面重新渲染的部分)洒扎,通常不會立即執(zhí)行辑甜,而是每16毫秒執(zhí)行一次。這時使用requestAnimationFrame()的效果要好于setTimeout()袍冷。
- 需要注意的是磷醋,setTimeout()只是將事件插入了"任務隊列",必須等到當前代碼(執(zhí)行棧)執(zhí)行完难裆,主線程才會去執(zhí)行它指定的回調(diào)函數(shù)。要是當前代碼耗時很長,有可能要等很久乃戈,所以并沒有辦法保證褂痰,回調(diào)函數(shù)一定會在setTimeout()指定的時間執(zhí)行。