很多文章在介紹線程以及線程之間的關係,都存在著脫節的現象。還有的文章過於廣大,涉及到了內核,本文希望以通俗易懂的話去描述晦澀的詞語,可能會和實際有一丟丟的出入,但是更易理解。
我們都知道JS是單線程的,即js的代碼只能在一個線程上運行,也就說,js同時只能執行一個js任務,但是為什麼要這樣呢?這與瀏覽器的用途有關,JS的主要用途是與用戶互動和操作DOM。設想一段JS代碼,分發到兩個並行互不相關的線程上運行,一個線程在DOM上添加內容,另一個線程在刪除DOM,那麼會發生什麼?以哪個為準?所以為了避免複雜性,JS從一開始就是單線程的,以後也不會變。
這裡我們已經知道了,一段JS代碼只能在一個線程從上到下的執行,但是我們遇到setTimeout或者ajax異步時,也沒有等待啊,往下看。
2. 瀏覽器
既然JS是單線程的,那麼諸如onclick回調,setTimeout,Ajax這些都是怎麼實現的呢?是因為瀏覽器或node(宿主環境)是多線程的,即瀏覽器搞了幾個其他線程去輔助JS線程的運行。
瀏覽器有很多線程,例如:
- GUI 渲染線程
- JS 引擎線程
- 定時器觸發線程 (setTimeout)
- 瀏覽器事件線程 (onclick)
- http 異步線程
- EventLoop輪詢處理線程
- ...
其中,1、2、4為常駐線程
接下來,我們對這些線程進行分類。
3. 線程與進程
什麼是進程?
我們可以在電腦的任務管理器中查看到正在運行的進程,可以認為一個進程就是在運行一個程序,比如用瀏覽器打開一個網頁,這就是開啟了一個進程。但是比如打開3個網頁,那麼就開啟了3個進程,我們這裡只研究打開一個網頁即一個進程。
一個 進程 的運行,當然需要很多個 線程 互相配合,比如打開QQ的這個進程,可能同時有接收消息線程、傳輸文件線程、檢測安全線程......所以一個網頁能夠正常的運行並和用戶交互,也需要很多個進程之間相互配合,而其主要的一些線程,剛才在上面已經列出來了,分類:
類別A:GUI 渲染線程
類別B:JS 引擎線程
類別C:EventLoop輪詢處理線程
類別D:其他線程,有 定時器觸發線程 (setTimeout)、http 異步線程、瀏覽器事件線程 (onclick)等等。
注意:類別A和類別B是互斥的,原因不用說了,不知道的看我上一篇文章。所以我們下面的討論,就不涉及類別A了,只討論類別B、C、D之間的關係。
類別B:
JS 引擎線程,我們把它稱為 主線程 ,它是幹嘛的?即運行JS代碼的那個線程(不包括異步的那些代碼),比如:
1 var a = 2;
2 setTimeout()
3 ajax()
4 console.log()
第1、4行代碼是同步代碼,直接在主線程中運行;第2、3行代碼交給其他線程運行。
主線程運行JS代碼時,會生成個 執行棧 ,可以處理函數的嵌套,通過出棧進棧這樣,這裡不做過多介紹,很多文章。
消息隊列(任務隊列)
可以理解為一個靜態的隊列存儲結構,非線程,只做存儲,裡面存的是一堆異步成功後的回調函數,肯定是先成功的異步的回調函數在隊列的前面,後成功的在後面。
注意:是異步成功後,才把其回調函數扔進隊列中,而不是一開始就把所有異步的回調函數扔進隊列。比如setTimeout 3秒後執行一個函數,那麼這個函數是在3秒後才進隊列的。
類別D:
JS代碼中,碰到異步代碼,就被放入相對應的線程中去執行,比如:
1 var a = 2;
2 setTimeout(fun A)
3 ajax(fun B)
4 console.log()
5 dom.onclick(func C)
主線程在運行這段代碼時,碰到2 setTimeout(fun A),把這行代碼交給 定時器觸發線程 去執行
碰到3 ajax(fun B),把這行代碼交給 http 異步線程 去執行
碰到5 dom.onclick(func C) ,把這行代碼交給 瀏覽器事件線程 去執行
注意:這幾個異步代碼的回調函數fun A,fun B,fun C,各自的線程都會保存著的,因為需要在未來執行啊。。。
所以,這幾個線程主要幹兩件事:
- 執行主線程扔過來的異步代碼,並執行代碼
- 保存著回調函數,異步代碼執行成功後,通知 EventLoop輪詢處理線程 過來取相應的回調函數
類別C:
EventLoop輪詢處理線程
上面我們已經知道了,有3個東西
- 主線程,處理同步代碼
- 類別D的線程,處理異步代碼
- 消息隊列,存儲著異步成功後的回調函數,一個靜態存儲結構
這裡再對消息隊列說一下,其作用就是存放著未來要執行的回調函數,比如
setTimeout(() => {
console.log(1)
}, 2000)
setTimeout(() => {
console.log(2)
}, 3000)
在一開始,消息隊列是空的,在2秒後,一個 () => { console.log(1) } 的函數進入隊列,在3秒後,一個 () => { console.log(2) }的函數進入隊列,此時隊列裡有兩個元素,主線程從隊列頭中挨個取出並執行。
到這裡我們就知道了,這3個東西大概的作用、關係和流程,但是,它們3個互相怎麼交流的?這需要一箇中介去專門去溝通它們3個,而這個中介,就是 EventLoop輪詢處理線程
既然叫輪詢了,那麼肯定是不斷的循環的去交流和溝通
圖畫的有點醜,但是大概是這個意思,從主線程那裡順時針的看。
注意整個的流程是循環往復的。
注意只有主線程的同步代碼都執行完了,才會去隊列裡看看還有啥要執行的沒
小區別
在異步線程類別D那裡,還有一些小區別:
主線程把setTimeout、ajax、dom.onclick分別給三個線程,他們之間有些不同
- 對於setTimeout代碼,定時器觸發線程在接收到代碼時就開始計時,時間到了將回調函數扔進隊列。
- 對於ajax代碼,http 異步線程立即發起http請求,請求成功後將回調函數扔進隊列。
- 對於dom.onclick,瀏覽器事件線程會先監聽dom,直到dom被點擊了,才將回調函數扔進隊列。
總體實例
var a = 111;
setTimeout(function() {
console.log(222)
}, 2000)
fetch(url) // 假設該http請求花了3秒鐘
.then(function() {
console.log(333)
})
dom.onclick = function() { // 假設用戶在4秒鐘時點擊了dom
console.log(444)
}
console.log(555)
// 結果
555
222
333
444
步驟1:
主線程只執行了var a = 111;和console.log(555)兩行代碼,其他的代碼分別交給了其他三個線程,因為其他線程需要2、3、4秒鐘才成功並回調,所以在2秒之前,主線程一直在空閒,不斷的探查隊列是否不為空。
此時主線程裡其實已經是空的了(因為執行完那兩行代碼了)
步驟2:
2秒鐘之後,setTimeout成功了
步驟3:
步驟4:
這三個圖忘畫 EventLoop 了,你應該知道在哪。。。
注意
圖裡的隊列裡都只有一個回調函數,實際上有很多個回調函數,如果主線程裡執行的代碼複雜需要很長時間,這時隊列裡的函數們就排著,等著主線程啥時執行完,再來隊列裡取
所以從這裡能看出來,對於setTimeout,setInterval的定時,不一定完全按照設想的時間的,因為主線程裡的代碼可能複雜到執行很久,所以會發生你定時3秒後執行,實際上是3.5秒後執行(主線程花費了0.5秒)
閱讀更多 BM小偉 的文章