Js多執行緒和Event Loop
引子
幾乎在每一本js相關的書籍中,都會說js是單執行緒的,JS是通過事件佇列(Event Loop)的方式來實現非同步回撥的。 對很多初學JS的人來說,根本搞不清楚單執行緒的JS為什麼擁有非同步的能力,所以,我試圖從程序、執行緒的角度來解釋這個問題。
CPU
說到CPU和程序、執行緒,對計算機作業系統有過學習和了解的同學應該比較熟悉。
計算機的核心是CPU,它承擔了所有的計算任務。
它就像一座工廠,時刻在執行。
假定工廠的電力有限,一次只能供給一個車間使用。 也就是說,一個車間開工的時候,其他車間都必須停工。 背後的含義就是,單個CPU一次只能執行一個任務。
程序就好比工廠的車間,它代表CPU所能處理的單個任務。程序之間相互獨立,任一時刻,CPU總是執行一個程序,其他程序處於非執行狀態。 CPU使用時間片輪轉進度演算法來實現同時執行多個程序。
CPU、程序、執行緒之間的關係
從上文我們已經簡單瞭解了CPU、程序、執行緒,簡單彙總一下。
- 程序是cpu資源分配的最小單位(是能擁有資源和獨立執行的最小單位)
- 執行緒是cpu排程的最小單位(執行緒是建立在程序的基礎上的一次程式執行單位,一個程序中可以有多個執行緒)
- 不同程序之間也可以通訊,不過代價較大
- 單執行緒與多執行緒,都是指在一個程序內的單和多
瀏覽器是多程序的
我們已經知道了CPU、程序、執行緒之間的關係,對於計算機來說,每一個應用程式都是一個程序, 而每一個應用程式都會分別有很多的功能模組,這些功能模組實際上是通過子程序來實現的。 對於這種子程序的擴充套件方式,我們可以稱這個應用程式是多程序的。
而對於瀏覽器來說,瀏覽器就是多程序的,我在Chrome瀏覽器中打開了多個tab,然後開啟windows控制管理器:
如上圖,我們可以看到一個Chrome瀏覽器啟動了好多個程序。
總結一下:
- 瀏覽器是多程序的
- 每一個Tab頁,就是一個獨立的程序
瀏覽器包含了哪些程序
-
主程序
- 協調控制其他子程序(建立、銷燬)
- 瀏覽器介面顯示,使用者互動,前進、後退、收藏
- 將渲染程序得到的記憶體中的Bitmap,繪製到使用者介面上
- 處理不可見操作,網路請求,檔案訪問等
-
第三方外掛程序
- 每種型別的外掛對應一個程序,僅當使用該外掛時才建立
-
GPU程序
- 用於3D繪製等
-
渲染程序,就是我們說的瀏覽器核心
- 負責頁面渲染,指令碼執行,事件處理等
- 每個tab頁一個渲染程序
那麼瀏覽器中包含了這麼多的程序,那麼對於普通的前端操作來說,最重要的是什麼呢?
答案是渲染程序,也就是我們常說的瀏覽器核心
瀏覽器核心(渲染程序)
從前文我們得知,程序和執行緒是一對多的關係,也就是說一個程序包含了多條執行緒。
而對於渲染程序來說,它當然也是多執行緒的了,接下來我們來看一下渲染程序包含哪些執行緒。
-
GUI渲染執行緒
- 負責渲染頁面,佈局和繪製
- 頁面需要重繪和迴流時,該執行緒就會執行
- 與js引擎執行緒互斥,防止渲染結果不可預期
-
JS引擎執行緒
- 負責處理解析和執行JavaScript指令碼程式
- 只有一個JS引擎執行緒(單執行緒)
- 與GUI渲染執行緒互斥,防止渲染結果不可預期
-
事件觸發執行緒
- 用來控制事件迴圈(滑鼠點選、setTimeout、ajax等)
- 當事件滿足觸發條件時,將事件放入到JS引擎所在的執行佇列中
-
定時觸發器執行緒
- setInterval與setTimeout所在的執行緒
- 定時任務並不是由JS引擎計時的,是由定時觸發執行緒來計時的
- 計時完畢後,通知事件觸發執行緒
-
非同步http請求執行緒
- 瀏覽器有一個單獨的執行緒用於處理AJAX請求
- 當請求完成時,若有回撥函式,通知事件觸發執行緒
當我們瞭解了渲染程序包含的這些執行緒後,我們思考兩個問題:
- 為什麼JavaScript是單執行緒的
- 為什麼 GUI 渲染執行緒為什麼與 JS 引擎執行緒互斥
為什麼 javascript 是單執行緒的
首先是歷史原因,在建立 javascript 這門語言時,多程序多執行緒的架構並不流行,硬體支援並不好。
其次是因為多執行緒的複雜性,多執行緒操作需要加鎖,編碼的複雜性會增高。
而且,如果同時操作 DOM ,在多執行緒不加鎖的情況下,最終會導致 DOM 渲染的結果不可預期。
為什麼 GUI 渲染執行緒與 JS 引擎執行緒互斥
這是由於 JS 是可以操作 DOM 的,如果同時修改元素屬性並同時渲染介面(即JS執行緒和UI執行緒同時執行), 那麼渲染執行緒前後獲得的元素就可能不一致了。
因此,為了防止渲染出現不可預期的結果,瀏覽器設定GUI渲染執行緒和JS引擎執行緒為互斥關係, 當JS引擎執行緒執行時GUI渲染執行緒會被掛起,GUI更新則會被儲存在一個佇列中等待JS引擎執行緒空閒時立即被執行。
從 Event Loop 看 JS 的執行機制
到了這裡,終於要進入我們的主題,什麼是 Event Loop
先理解一些概念:
- JS 分為同步任務和非同步任務
- 同步任務都在JS引擎執行緒上執行,形成一個執行棧
- 事件觸發執行緒管理一個任務佇列,非同步任務觸發條件達成,將回調事件放到任務佇列中
- 執行棧中所有同步任務執行完畢,此時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引擎執行緒只執行執行棧中的事件
- 執行棧中的程式碼執行完畢,就會讀取事件佇列中的事件
- 事件佇列中的回撥事件,是由各自執行緒插入到事件佇列中的
- 如此迴圈
資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com
巨集任務、微任務
當我們基本瞭解了什麼是執行棧,什麼是事件佇列之後,我們深入瞭解一下事件迴圈中巨集任務、微任務
什麼是巨集任務
我們可以將每次執行棧執行的程式碼當做是一個巨集任務(包括每次從事件佇列中獲取一個事件回撥並放到執行棧中執行), 每一個巨集任務會從頭到尾執行完畢,不會執行其他。
我們前文提到過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執行緒繼續接管,開始下一個巨集任務(從事件佇列中獲取