1. 前言
很多文章在介紹執行緒以及執行緒之間的關係,都存在著脫節的現象。還有的文章過於廣大,涉及到了核心,本文希望以通俗易懂的話去描述晦澀的詞語,可能會和實際有一丟丟的出入,但是更易理解。
我們都知道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:
定時器觸發執行緒 (setTimeout)、http 非同步執行緒、瀏覽器事件執行緒 (onclick)
主執行緒執行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被點選了,才將回撥函式扔進佇列。
4. 總體例項
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:
注意
圖裡的佇列裡都只有一個回撥函式,實際上有很多個回撥函式,如果主執行緒裡執行的程式碼複雜需要很長時間,這時佇列裡的函式們就排著,等著主執行緒啥時執行完,再來佇列裡取
所以從這裡能看出來,對於setTimeout,setInterval的定時,不一定完全按照設想的時間的,因為主執行緒裡的程式碼可能複雜到執行很久,所以會發生你定時3秒後執行,實際上是3.5秒後執行(主執行緒花費了0.5秒)
之後我會再寫如何解決定時誤差的內容。。。
借兩個經典的圖
setTimeout、setInterval
關於這兩個的延遲和解決辦法,看這篇文章,也是經常考的一個知識點!!!
~~
最後:若有錯誤之處,還請見諒,提出後會馬上修改~~~
轉載請註明出處,謝謝~~