前端之JS的線程

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

我們都知道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輪詢處理線程
  7. ...

其中,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,各自的線程都會保存著的,因為需要在未來執行啊。。。

所以,這幾個線程主要幹兩件事:

  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被點擊了,才將回調函數扔進隊列。

總體實例

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的線程

這三個圖忘畫 EventLoop 了,你應該知道在哪。。。

注意

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

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


分享到:


相關文章: