JS三座大山再學習(三、非同步和單執行緒)

笨拙發表於2019-12-02

本文已釋出在西瓜君的個人部落格,原文傳送門

前言

寫這一篇的時候,西瓜君查閱了很多資料和文章,但是相當多的文章寫的都很簡單,甚至互相之間有矛盾,這讓我很困擾;同時也讓我堅定了要寫出一篇好的關於JS非同步、單執行緒、事件迴圈的文章,下面,讓我們一起來學習本文吧,衝鴨~~

單執行緒

1. 什麼是單執行緒

//栗子1
console.log(1)
console.log(2)
console.log(3)
//輸出順序 1 2 3
複製程式碼

單執行緒即同一時間只做一件事

2. JavaScript為什麼是單執行緒

  • 首先是歷史原因,在建立 javascript 這門語言時,多程式多執行緒的架構並不流行,硬體支援並不好。
  • 其次是因為多執行緒的複雜性,多執行緒操作需要加鎖,編碼的複雜性會增高。
  • 而且,如果同時操作 DOM ,在多執行緒不加鎖的情況下,最終會導致 DOM 渲染的結果不可預期

為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。

非同步

1.JS的 同步任務/非同步任務

  • 同步任務:在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務

  • 非同步:不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行

//非同步的栗子
console.log(1)

setTimeout(()=>{
    console.log(2)
},100)

console.log(3)
//輸出順序 1 3 2
複製程式碼

2. JavaScript為什麼需要非同步

如果在JS程式碼執行過程中,某段程式碼執行過久,後面的程式碼遲遲不能執行,產生阻塞(即卡死),會影響使用者體驗。

JavaScript怎麼實現非同步

JS 實現非同步時通過 事件迴圈(Event Loop),下面我們來了解一下

1.執行棧與任務佇列

先理解幾個概念

  • JS任務 分為同步任務(synchronous)和非同步任務(asynchronous)
  • 同步任務都在 JS引擎執行緒(主執行緒) 上執行,形成一個執行棧(call stack)
  • 事件觸發執行緒 管理一個 任務佇列(Task Queue)
  • 非同步任務 觸發條件達成,將 回撥事件 放到任務佇列(Task Queue)中
  • 執行棧中所有同步任務執行完畢,此時JS引擎執行緒空閒,系統會讀取任務佇列,將可執行的非同步任務回撥事件新增到執行棧中,開始執行

當一個JS檔案第一次執行的時候,js引擎會 解析這段程式碼,並將其中的同步程式碼 ***按照執行順序加入執行棧***中,然後從頭開始執行。如果當前執行的是一個方法,那麼js會向執行棧中新增這個方法的執行環境,然後進入這個執行環境繼續執行其中的程式碼。當這個執行環境中的程式碼 執行完畢並返回結果後,js會退出這個執行環境並把這個執行環境銷燬,回到上一個方法的執行環境。這個過程反覆進行,直到執行棧中的程式碼全部執行完畢。

舉個例子:

//Event loop

//(1)
console.log(1)

//(2)
setTimeout(()=>{
    console.log(2)
},100)

//(3)
console.log(3)
複製程式碼
  1. 先解析整段程式碼,按照順序加入到執行棧中,從頭開始執行
  2. 先執行(1),是同步的,所以直接列印 1
  3. 執行(2),發現是 setTimeout,於是呼叫瀏覽器的方法(webApi)執行,在 100ms後將 console.log(2) 加入到任務佇列
  4. 執行(3),同步的,直接列印 3
  5. 執行棧已經清空了,現在檢查任務佇列,(執行太快的話可能此時任務佇列還是空的,沒到100ms,還沒有將(2)的列印加到任務佇列,於是不停的檢測,直到佇列中有任務),發現有 console.log(2),於是新增到執行棧,執行console.log(2),同步程式碼,直接列印 2 (如果這裡是非同步任務,同樣會再走一遍迴圈:-->任務佇列->執行棧)

所以結果是 1 3 2

注:setTimeout/Promise等我們稱之為任務源。而進入任務佇列的是他們指定的回撥

2.巨集任務(macro task)與微任務(micro task)

上面的迴圈只是一個巨集觀的表述,實際上非同步任務之間也是有不同的,分為 巨集任務(macro task) 與 微任務(micro task),最新的標準中,他們被稱為 taskjobs

  • 巨集任務有哪些:script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering(渲染)
  • 微任務有哪些:process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)

下面我們再詳細講解一下執行過程

執行棧在執行的時候,會把巨集任務放在一個巨集任務的任務佇列,把微任務放在一個微任務的任務佇列,在當前執行棧為空的時候,主執行緒會 檢視微任務佇列是否有事件存在。如果微任務佇列不存在,那麼會去巨集任務佇列中 取出一個任務 加入當前執行棧;如果微任務佇列存在,則會依次執行微任務佇列中的所有任務,直到微任務佇列為空(同樣,是吧佇列中的事件加到執行棧執行),然後去巨集任務佇列中取出最前面的一個事件加入當前執行棧...如此反覆,進入迴圈。

注:

  • 巨集任務和微任務的任務佇列都可以有多個
  • 當前執行棧執行完畢時會立刻先處理所有微任務佇列中的事件,然後再去巨集任務佇列中取出一個事件。同一次事件迴圈中,微任務永遠在巨集任務之前執行。
  • 不同的執行環境 迴圈策略可能有不同,這裡探討chrome、node環境

舉個例子:

//Event loop


//(1)
setTimeout(()=>{
    console.log(1)
},100)

//(2)
setTimeout(()=>{
    console.log(2)
},100)

//(3)
new Promise(function(resolve,reject){
    //(4)
    console.log(3)
    resolve(4)
}).then(function(val){
    //(5)
    console.log(val);
})

//(6)
new Promise(function(resolve,reject){
    //(7)
    console.log(5)
    resolve(6)
}).then(function(val){
    //(8)
    console.log(val);
})

//(9)
console.log(7)

//(10)
setTimeout(()=>{
    console.log(8)
},50)
複製程式碼

*上面的程式碼在node和chrome環境的正確列印順序是 3 5 7 4 6 8 1 2

下面分析一下執行過程:

  1. 全部程式碼在解析後加入執行棧
  2. 執行(1),巨集任務,呼叫webapi setTimeout,這個方法會在100ms後將回撥函式放入巨集任務的任務佇列
  3. 執行(2),同(1),但是會比(1)稍後一點
  4. 執行(3),同步執行new Promise,然後執行(4),直接列印 3 ,然後resolve(4),然後.then(),把(5)放入微任務的任務佇列
  5. 執行(6),同上,先列印 5 ,再執行resolve(6),然後.then()裡面的內容(8)加入到微任務的任務佇列
  6. 執行(9),同步程式碼,直接列印 7
  7. 執行(10),同(1)和(2),只是時間更短,會在 50ms 後將回撥 console.log(8) 加入巨集任務的任務佇列
  8. 現在執行棧清空了,開始檢查微任務佇列,發現(5),加入到執行棧執行,是同步程式碼,直接列印 4
  9. 任務佇列又執行完了,又檢查微任務佇列,發現(8),列印 6
  10. 任務佇列又執行完了,檢查微任務佇列,沒有任務,再檢查巨集任務佇列,此時如果超過了50ms的話,會發現 console.log(8) 在巨集任務佇列中,於是執行 列印 8
  11. 依次列印 1 2

注:因為渲染也是巨集任務,需要在一次執行棧執行完後才會執行渲染,所以如果執行棧中同時有幾個同步的改變同一個樣式的程式碼,在渲染時只會渲染最後一個

結語

寫到這裡,仍然覺得還有很多知識點沒有寫出來,但是想寫又不知道從哪裡入手。於是決定今天就寫到這裡,日後再做補充。 到這篇,JS三座大山系列就暫時完結了,在這其中自己也學到了很多,希望能繼續輸出一些有意義的東西,加油,西瓜君~~

參考文章:

www.jianshu.com/p/12b9f73c5… www.cnblogs.com/cangqinglan…

如有錯誤,請斧正

以上

相關文章