同步和異步操作的區別就是是否阻礙后續代碼的執行。
同步任務是那些沒有被引擎掛起、在主線程上排隊執行的任務。只有前一個任務執行完畢,才能執行后一個任務。
異步任務是那些被引擎放在一邊,不進入主線程、而進入任務隊列的任務。只有引擎任務某個異步任務可以執行了(比如Ajax操作從服務器得到了結果)【發布訂閱】,該任務(采用回調函數形式)才會進入主線程執行。排在異步任務后面的代碼,不用等待異步任務結束會馬上運行,也就是說,異步不具有“堵塞”效應。
在setTimeout的執行形式上來看,setTimeout是不會阻礙其后續代碼的執行的。所以可以理解為setTimeout是異步操作。
單線程模式
js是單線程的,但JS運行環境(Chrome瀏覽器)是多線程的。
GUI線程
GUI線程就是渲染頁面的,負責解析HTML和CSS,然后將她們構建成DOM樹和渲染樹就是這個線程負責的。
JS引擎線程
這個線程就是負責執行js的主線程,前面說的“JS是單線程的”就是指的這個線程。V8引擎就是在這個線程運行。需要注意的是,這個線程跟GUI線程是互斥的。互斥的原因是JS也可以操作DOM,如果JS線程和GUI線程同時操作DOM,結果就混亂了,不知道到底渲染哪個結果。這帶來的后果就是如果JS長時間運行,GUI線程就不能執行,整個頁面就感覺卡死了。所以我們最開始例子的while(true)這樣長時間的同步代碼在真正開發時是絕對不允許的。
定時器線程
定時器線程其實只是一個計時的作用,它並不會真正執行事件到了的回調,真正執行這個回調的還是Js主線程。所以當時間到了定時器線程會將這個回調事件給到事件觸發線程,然后事件觸發線程將它加到任務隊列里面去。最終JS主線程從任務隊列取出這個回調執行。事件觸發線程不僅會將定時器事件放入任務隊列,其他滿足條件的事件也是由它負責放進任務隊列。
異步HTTP請求線程
這個線程負責處理異步的Ajax請求,當請求完成后,它也會通知事件觸發線程,然后事件觸發線程將這個事件放入任務隊列給主線程執行。
所以異步的實現靠的就是瀏覽器的多線程,當他遇到異步API時,就將這個任務交給對應的線程,當這個異步API滿足回調條件時,對應的線程又通過事件觸發線程將這個事件放入任務隊列,然后主線程從任務隊列取出事件繼續執行。 這個流程我們多次提到了任務隊列,這其實就是Event Loop。
任務隊列(Event Loop)
JavaScript 運行時,除了一個正在運行的主線程,引擎還提供多個任務隊列(根據任務的類型,所以有多個)。
首先,主線程會去執行所有的同步任務。等到同步任務全部執行完,就會去看任務隊列里面有沒有事件回調。如果有,則取出就重新進入主線程執行,這時它就變成同步任務了。等到執行完,下一個異步任務再進入主線程開始執行。一旦任務隊列清空,程序就結束執行。
異步任務的寫法通常是回調函數。一旦異步任務重新進入主線程,就會執行對應的回調函數。如果一個異步任務沒有回調函數,就不會進入任務隊列,也就是說,不會重新進入主線程,因為沒有用回調函數指定下一步的操作。
只要同步任務執行完了,引擎就會一遍又一遍地去檢查那些掛起來的異步任務,是不是可以進入主線程了。這種循環檢查的機制,就叫做事件循環(Event Loop)
目前JS的主要運行環境有兩個——瀏覽器和Node.js。相比於V8引擎,node 中增加了兩種異步方式: process.nextTick() 和 setImmediate()。
Node 規定,process.nextTick()和Promise的回調函數,追加在本輪循環,即同步任務一旦執行完成,就開始執行它們。
而setTimeout、setInterval、setImmediate的回調函數,追加在次輪循環。
process.nextTick()
追加到本輪循環執行,而且是所有異步任務里面最快執行的,nextTickQueue。
Promise異步
Promise的回調函數不是正常的異步任務,而是微任務(microtask)。它們的區別在於,正常任務追加到下一輪事件循環,微任務隊列追加在process.nextTick隊列的后面,也屬於本輪循環。
Nodejs事件循環的六個階段
首先,事件循環是在主線程上完成的。其次,腳本開始執行時,事件循環只進行了初始化,並沒有開始。只有當下面事件執行完畢后,事件循環才會開始。
- 同步任務
- 發出異步請求
- 規定定時器生效的事件
- 執行process.nextTick()等等
事件循環會無限次地執行,一輪又一輪。只有異步任務的回調函數隊列清空了,才會停止執行。
每一輪的事件循環,分成六個階段。
timers:定時器階段,處理setTimeout()和setInterval()的回調函數。進入這個階段后,主線程會檢查一下當前時間,是否滿足定時器的條件。如果滿足就執行回調函數,否則就離開這個階段。
I/O callbacks:除了以下操作的回調函數,其他的回調函數都在這個階段執行。【setTimeout()和setInterval()的回調函數;setImmediate()的回調函數;用於關閉請求的回調函數,比如socket.on('close', ...)】
idle, prepare:這個階段是輪詢時間,用於等待還未返回的 I/O 事件,比如服務器的回應、用戶移動鼠標等等。
這個階段的時間會比較長。如果沒有其他異步任務要處理(比如到期的定時器),會一直停留在這個階段,等待 I/O 請求返回結果。
poll:這個階段是輪詢時間,用於等待還未返回的 I/O 事件,比如服務器的回應、用戶移動鼠標等等。
check:該階段執行setImmediate()的回調函數。
close callbacks:該階段執行關閉請求的回調函數,比如socket.on('close', ...)
每個階段都有一個先進先出的回調函數隊列。只有一個階段的回調函數隊列清空了,該執行的回調函數都執行了,事件循環才會進入下一個階段。