幾乎在每一本JS相關的書籍中,都會說JS是單線程的,JS是經過事件隊列(Event Loop)的方式來實現異步回調的。 對不少初學JS的人來講,根本搞不清楚單線程的JS爲何擁有異步的能力,因此,我試圖從進程、線程的角度來解釋這個問題。javascript
說到CPU和進程、線程,對計算機操做系統有過學習和了解的同窗應該比較熟悉。前端
計算機的核心是CPU
,它承擔了全部的計算任務。java
它就像一座工廠,時刻在運行。ajax
假定工廠的電力有限,一次只能供給一個車間使用。 也就是說,一個車間開工的時候,其餘車間都必須停工。 背後的含義就是,單個CPU一次只能運行一個任務。算法
進程
就比如工廠的車間,它表明CPU所能處理的單個任務。 進程
之間相互獨立,任一時刻,CPU老是運行一個進程
,其餘進程
處於非運行狀態。 CPU使用時間片輪轉進度算法來實現同時運行多個進程
。編程
從上文咱們已經簡單瞭解了CPU、進程、線程,簡單彙總一下。segmentfault
進程
是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)線程
是cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位,一個進程中能夠有多個線程)進程
之間也能夠通訊,不過代價較大單線程
與多線程
,都是指在一個進程
內的單和多咱們已經知道了CPU
、進程
、線程
之間的關係,對於計算機來講,每個應用程序都是一個進程
, 而每個應用程序都會分別有不少的功能模塊,這些功能模塊其實是經過子進程
來實現的。 對於這種子進程
的擴展方式,咱們能夠稱這個應用程序是多進程
的。windows
而對於瀏覽器來講,瀏覽器就是多進程的,我在Chrome瀏覽器中打開了多個tab,而後打開windows控制管理器:promise
如上圖,咱們能夠看到一個Chrome瀏覽器啓動了好多個進程。
總結一下:瀏覽器
主進程
第三方插件進程
GPU進程
渲染進程
,就是咱們說的瀏覽器內核
那麼瀏覽器中包含了這麼多的進程,那麼對於普通的前端操做來講,最重要的是什麼呢?
答案是渲染進程
,也就是咱們常說的瀏覽器內核
從前文咱們得知,進程和線程是一對多的關係,也就是說一個進程包含了多條線程。
而對於渲染進程
來講,它固然也是多線程的了,接下來咱們來看一下渲染進程包含哪些線程。
GUI渲染線程
JS引擎線程
事件觸發線程
定時觸發器線程
異步http請求線程
當咱們瞭解了渲染進程包含的這些線程後,咱們思考兩個問題:
首先是歷史緣由,在建立 javascript 這門語言時,多進程多線程的架構並不流行,硬件支持並很差。
其次是由於多線程的複雜性,多線程操做須要加鎖,編碼的複雜性會增高。
並且,若是同時操做 DOM ,在多線程不加鎖的狀況下,最終會致使 DOM 渲染的結果不可預期。
這是因爲 JS 是能夠操做 DOM 的,若是同時修改元素屬性並同時渲染界面(即 JS線程
和UI線程
同時運行), 那麼渲染線程先後得到的元素就可能不一致了。
所以,爲了防止渲染出現不可預期的結果,瀏覽器設定 GUI渲染線程
和JS引擎線程
爲互斥關係, 當JS引擎線程
執行時GUI渲染線程
會被掛起,GUI更新則會被保存在一個隊列中等待JS引擎線程
空閒時當即被執行。
到了這裏,終於要進入咱們的主題,什麼是 Event Loop
先理解一些概念:
執行棧
任務隊列
,異步任務觸發條件達成,將回調事件放到任務隊列
中執行棧
中全部同步任務執行完畢,此時JS引擎線程空閒,系統會讀取任務隊列
,將可運行的異步任務回調事件添加到執行棧
中,開始執行在前端開發中咱們會經過setTimeout/setInterval
來指定定時任務,會經過XHR/fetch
發送網絡請求, 接下來簡述一下setTimeout/setInterval
和XHR/fetch
到底作了什麼事
咱們知道,不論是setTimeout/setInterval
和XHR/fetch
代碼,在這些代碼執行時, 自己是同步任務,而其中的回調函數纔是異步任務。
當代碼執行到setTimeout/setInterval
時,其實是JS引擎線程
通知定時觸發器線程
,間隔一個時間後,會觸發一個回調事件, 而定時觸發器線程
在接收到這個消息後,會在等待的時間後,將回調事件放入到由事件觸發線程
所管理的事件隊列
中。
當代碼執行到XHR/fetch
時,其實是JS引擎線程
通知異步http請求線程
,發送一個網絡請求,並制定請求完成後的回調事件, 而異步http請求線程
在接收到這個消息後,會在請求成功後,將回調事件放入到由事件觸發線程
所管理的事件隊列
中。
當咱們的同步任務執行完,JS引擎線程
會詢問事件觸發線程
,在事件隊列
中是否有待執行的回調函數,若是有就會加入到執行棧中交給JS引擎線程
執行
用一張圖來解釋:
再用代碼來解釋一下:
let timerCallback = function() { console.log('wait one second'); }; let httpCallback = function() { console.log('get server data success'); } // 同步任務 console.log('hello'); // 同步任務 // 通知定時器線程 1s 後將 timerCallback 交由事件觸發線程處理 // 1s 後事件觸發線程將 timerCallback 加入到事件隊列中 setTimeout(timerCallback,1000); // 同步任務 // 通知異步http請求線程發送網絡請求,請求成功後將 httpCallback 交由事件觸發線程處理 // 請求成功後事件觸發線程將 httpCallback 加入到事件隊列中 $.get('www.xxxx.com',httpCallback); // 同步任務 console.log('world'); //... // 全部同步任務執行完後 // 詢問事件觸發線程在事件事件隊列中是否有須要執行的回調函數 // 若是沒有,一直詢問,直到有爲止 // 若是有,將回調事件加入執行棧中,開始執行回調代碼
總結一下:
當咱們基本瞭解了什麼是執行棧,什麼是事件隊列以後,咱們深刻了解一下事件循環中宏任務、微任務
咱們能夠將每次執行棧執行的代碼當作是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行), 每個宏任務會從頭至尾執行完畢,不會執行其餘。
咱們前文提到過JS引擎線程
和GUI渲染線程
是互斥的關係,瀏覽器爲了可以使宏任務
和DOM任務
有序的進行,會在一個宏任務
執行結果後,在下一個宏任務
執行前,GUI渲染線程
開始工做,對頁面進行渲染。
// 宏任務-->渲染-->宏任務-->渲染-->渲染...
主代碼塊,setTimeout,setInterval等,都屬於宏任務
第一個例子:
document.body.style = 'background:black'; document.body.style = 'background:red'; document.body.style = 'background:blue'; document.body.style = 'background:grey';
咱們能夠將這段代碼放到瀏覽器的控制檯執行如下,看一下效果:
咱們會看到的結果是,頁面背景會在瞬間變成灰色,以上代碼屬於同一次宏任務,因此所有執行完才觸發頁面渲染,渲染時GUI線程會將全部UI改動優化合並,因此視覺效果上,只會看到頁面變成灰色。
第二個例子:
document.body.style = 'background:blue'; setTimeout(function(){ document.body.style = 'background:black' },0)
執行一下,再看效果:
我會看到,頁面先顯示成藍色背景,而後瞬間變成了黑色背景,這是由於以上代碼屬於兩次宏任務,第一次宏任務執行的代碼是將背景變成藍色,而後觸發渲染,將頁面變成藍色,再觸發第二次宏任務將背景變成黑色。
咱們已經知道宏任務
結束後,會執行渲染,而後執行下一個宏任務
, 而微任務能夠理解成在當前宏任務
執行後當即執行的任務。
也就是說,當宏任務
執行完,會在渲染前,將執行期間所產生的全部微任務
都執行完。
Promise,process.nextTick等,屬於微任務
。
第一個例子:
document.body.style = 'background:blue' console.log(1); Promise.resolve().then(()=>{ console.log(2); document.body.style = 'background:black' }); console.log(3);
執行一下,再看效果:
控制檯輸出 1 3 2 , 是由於 promise 對象的 then 方法的回調函數是異步執行,因此 2 最後輸出
頁面的背景色直接變成黑色,沒有通過藍色的階段,是由於,咱們在宏任務中將背景設置爲藍色,但在進行渲染前執行了微任務, 在微任務中將背景變成了黑色,而後才執行的渲染。
第二個例子:
setTimeout(() => { console.log(1) Promise.resolve(3).then(data => console.log(data)) }, 0) setTimeout(() => { console.log(2) }, 0) // print : 1 3 2
上面代碼共包含兩個 setTimeout ,也就是說除主代碼塊外,共有兩個宏任務
, 其中第一個宏任務
執行中,輸出 1 ,而且建立了微任務隊列
,因此在下一個宏任務
隊列執行前, 先執行微任務
,在微任務
執行中,輸出 3 ,微任務執行後,執行下一次宏任務
,執行中輸出 2
宏任務
(棧中沒有就從事件隊列
中獲取)微任務
,就將它添加到微任務
的任務隊列中宏任務
執行完畢後,當即執行當前微任務隊列
中的全部微任務
(依次執行)宏任務
執行完畢,開始檢查渲染,而後GUI線程
接管渲染JS線程
繼續接管,開始下一個宏任務
(從事件隊列中獲取本文轉載至掘金,做者雲中君,感謝做者分享。
http://www.javashuo.com/article/p-slsqhrtr-y.html
文章本身反覆閱讀了好幾遍,以爲仍是寫得很是不錯,由淺入深,通俗易懂,我想就算沒有必定計算機操做系統理論基礎的同窗也能夠看得懂了。
這實際上是一個js很是基礎同時很重要的知識點,js事件執行機制的學習對js的異步編程的理解、瀏覽器性能優化都頗有幫助。
最後再次感謝做者!
推薦閱讀:
【專題:JavaScript進階之路】
JavaScript之深刻理解閉包
ES6 尾調用和尾遞歸
Git經常使用命令小結
JavaScript之call()理解
JavaScript之對象屬性
我是Cloudy,年輕的前端攻城獅一枚,愛專研,愛技術,愛分享。
我的筆記,整理不易,感謝閱讀、點贊和收藏。
文章有任何問題歡迎你們指出,也歡迎你們一塊兒交流前端各類問題!