幾乎在每一本JS相關的書籍中,都會說JS是單線程的,JS是經過事件隊列(Event Loop)的方式來實現異步回調的。 對不少初學JS的人來講,根本搞不清楚單線程的JS爲何擁有異步的能力,因此,我試圖從進程、線程的角度來解釋這個問題。
javascript
它就像一座工廠,時刻在運行。html
從上文咱們已經簡單瞭解了CPU、進程、線程,簡單彙總一下前端
咱們已經知道了CPU、進程、線程之間的關係,對於計算機來講,每個應用程序都是一個進程, 而每個應用程序都會分別有不少的功能模塊,這些功能模塊其實是經過子進程來實現的。 對於這種子進程的擴展方式,咱們能夠稱這個應用程序是多進程的。
而對於瀏覽器來講,瀏覽器就是多進程的,我在Chrome瀏覽器中打開了多個tab,而後打開mac中的活動監視器: java
答案:固然是渲染進程啦,也就是咱們常說的瀏覽器內核ajax
從前文咱們得知,進程和線程是一對多的關係,也就是說一個進程包含了多條線程。
而對於渲染進程來講,它固然也是多線程的了,接下來咱們來看一下渲染進程包含哪些線程。算法
首先是歷史緣由,在建立 javascript 這門語言時,多進程多線程的架構並不流行,硬件支持並很差。
其次是由於多線程的複雜性,多線程操做須要加鎖,編碼的複雜性會增高。
並且,若是同時操做 DOM ,在多線程不加鎖的狀況下,最終會致使 DOM 渲染的結果不可預期。segmentfault
這是因爲 JS 是能夠操做 DOM 的,若是同時修改元素屬性並同時渲染界面(即 JS線程和UI線程同時運行), 那麼渲染線程先後得到的元素就可能不一致了。
所以,爲了防止渲染出現不可預期的結果,瀏覽器設定 GUI渲染線程和JS引擎線程爲互斥關係, 當JS引擎線程執行時GUI渲染線程會被掛起,GUI更新則會被保存在一個隊列中等待JS引擎線程空閒時當即被執行。promise
到了這裏,終於要進入咱們的主題,什麼是 Event Loop
先理解一些概念:
瀏覽器
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');
全部同步任務執行完後
詢問事件觸發線程在事件事件隊列中是否有須要執行的回調函數
若是沒有,一直詢問,直到有爲止
若是有,將回調事件加入執行棧中,開始執行回調代碼
複製代碼
總結一下:bash
當咱們基本瞭解了什麼是執行棧,什麼是事件隊列以後,咱們深刻了解一下事件循環中宏任務、微任務
咱們能夠將每次執行棧執行的代碼當作是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行), 每個宏任務會從頭至尾執行完畢,不會執行其餘。
咱們前文提到過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
看到這裏,不知道對JS的運行機制是否是更加理解了,從頭至尾梳理,而不是就某一個碎片化知識應該是會更清晰的吧?
同時,也應該注意到了JS根本就沒有想象的那麼簡單,前端的知識也是無窮無盡,層出不窮的概念、N多易忘的知識點、各式各樣的框架、 底層原理方面也是能夠無限的往下深挖,而後你就會發現,你知道的太少了。。。
最後,喜歡的話,就請給個贊吧!