前端之JS的執行緒(最易懂)

晴天633發表於2019-01-04

1. 前言

很多文章在介紹執行緒以及執行緒之間的關係,都存在著脫節的現象。還有的文章過於廣大,涉及到了核心,本文希望以通俗易懂的話去描述晦澀的詞語,可能會和實際有一丟丟的出入,但是更易理解。

我們都知道JS是單執行緒的,即js的程式碼只能在一個執行緒上執行,也就說,js同時只能執行一個js任務,但是為什麼要這樣呢?這與瀏覽器的用途有關,JS的主要用途是與使用者互動和操作DOM。設想一段JS程式碼,分發到兩個並行互不相關的執行緒上執行,一個執行緒在DOM上新增內容,另一個執行緒在刪除DOM,那麼會發生什麼?以哪個為準?所以為了避免複雜性,JS從一開始就是單執行緒的,以後也不會變。

這裡我們已經知道了,一段JS程式碼只能在一個執行緒從上到下的執行,但是我們遇到setTimeout或者ajax非同步時,也沒有等待啊,往下看。

2. 瀏覽器

既然JS是單執行緒的,那麼諸如onclick回撥,setTimeout,Ajax這些都是怎麼實現的呢?是因為瀏覽器或node(宿主環境)是多執行緒的,即瀏覽器搞了幾個其他執行緒去輔助JS執行緒的執行。

瀏覽器有很多執行緒,例如:

  1. GUI 渲染執行緒

  2. JS 引擎執行緒

  3. 定時器觸發執行緒 (setTimeout)

  4. 瀏覽器事件執行緒 (onclick)

  5. http 非同步執行緒

  6. 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,各自的執行緒都會儲存著的,因為需要在未來的某個時候,將回撥函式交給主執行緒去執行啊。。。

所以,這幾個執行緒主要幹兩件事:

  1. 執行主執行緒扔過來的非同步程式碼,並執行程式碼
  2. 儲存回撥函式,在未來的某個時刻,通知EventLoop輪詢處理執行緒過來取相應的回撥函式然後執行(下面會講)

類別C:

EventLoop輪詢處理執行緒

上面我們已經知道了,有3個東西

  1. 主執行緒,處理同步程式碼
  2. 類別D的幾個非同步執行緒,處理非同步程式碼
  3. 訊息佇列,儲存著非同步成功後的回撥函式,一個靜態儲存結構

這裡再對訊息佇列說一下,其作用就是存放著未來要執行的回撥函式,比如

setTimeout(() => {
    console.log(1)
}, 2000)
setTimeout(() => {
    console.log(2)
}, 3000)
複製程式碼

在一開始,訊息佇列是空的,在2秒後,一個 () => { console.log(1) } 的函式進入佇列,在3秒後,一個 () => { console.log(2) }的函式進入佇列,此時佇列裡有兩個元素,主執行緒從佇列頭中挨個取出並執行。

到這裡我們就知道了,這3個東西大概的作用、關係和流程,但是,它們3個互相怎麼交流的?這需要一箇中介去專門去溝通它們3個,而這個中介,就是EventLoop輪詢處理執行緒

既然叫輪詢了,那麼肯定是不斷的迴圈的去交流和溝通

前端之JS的執行緒(最易懂)

圖畫的有點醜,但是大概是這個意思,從主執行緒那裡順時針的看。

注意整個的流程是迴圈往復的。

注意只有主執行緒的同步程式碼都執行完了,才會去佇列裡看看還有啥要執行的沒

小區別

在非同步執行緒類別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:

前端之JS的執行緒(最易懂)

主執行緒只執行了var a = 111;和console.log(555)兩行程式碼,其他的程式碼分別交給了其他三個執行緒,因為其他執行緒需要2、3、4秒鐘才成功並回撥,所以在2秒之前,主執行緒一直在空閒,不斷的探查佇列是否不為空。

此時主執行緒裡其實已經是空的了(因為執行完那兩行程式碼了)

步驟2:

2秒鐘之後,setTimeout成功了

前端之JS的執行緒(最易懂)

步驟3:

前端之JS的執行緒(最易懂)

步驟4:

前端之JS的執行緒(最易懂)

注意

圖裡的佇列裡都只有一個回撥函式,實際上有很多個回撥函式,如果主執行緒裡執行的程式碼複雜需要很長時間,這時佇列裡的函式們就排著,等著主執行緒啥時執行完,再來佇列裡取

前端之JS的執行緒(最易懂)

所以從這裡能看出來,對於setTimeout,setInterval的定時,不一定完全按照設想的時間的,因為主執行緒裡的程式碼可能複雜到執行很久,所以會發生你定時3秒後執行,實際上是3.5秒後執行(主執行緒花費了0.5秒)

之後我會再寫如何解決定時誤差的內容。。。

借兩個經典的圖

前端之JS的執行緒(最易懂)

前端之JS的執行緒(最易懂)

setTimeout、setInterval

關於這兩個的延遲和解決辦法,看這篇文章,也是經常考的一個知識點!!!

關於setInterval()你所不知道的地方

~~

最後:若有錯誤之處,還請見諒,提出後會馬上修改~~~

轉載請註明出處,謝謝~~

相關文章