我先假設一個場景,韓梅梅和李雷去飲品店買咖啡,每個人都想買兩杯咖啡,店里只有一個服務員,假設做一杯咖啡需要1分鐘時間。服務員可以有以下兩種提供咖啡的方案。
方案1:1分鐘后做好第一杯咖啡給韓梅梅,2分鐘后做好第二杯咖啡給韓梅梅。3分鐘后做好第三杯咖啡給李雷,4分鐘后做好第四杯咖啡給李雷。
方案2:1分鐘后做好第一杯咖啡給韓梅梅,2分鐘后做好第二杯咖啡給李雷。3分鐘后做好第三杯咖啡給韓梅梅,4分鐘后做好第四杯咖啡給李雷。
如上方案1即可理解為單線程,就是一件事情完全做完了才開始做下一件事情,在做一件事情的時候不會插入另外的事情。
如上方案2即可理解為多線程,就是一件事情可以被暫停,然后做其它事情,多件事情交替進行。在上面的場景中,還可以通過增加店小二的人數來提高效率,就好比計算機由單核CPU增加到雙核CPU。
JavaScript的代碼執行是單線程的。
JavaScript的代碼執行為什么不是多線程的?
因為JavaScript的目的是為了實現與用戶的動態交互,不可避免的會操作DOM元素。我們假設在一個線程刪除某DOM元素,在另外一個線程修改某DOM元素。如果是多線程的,這兩個任務可以同時執行,那最后呈現給用戶的到底是該DOM元素被修改了還是被刪除了呢?
JavaScript的代碼執行雖然是單線程的,但它的API卻提供了同步和異步兩種模式。
同步模式就是從上往下依次執行代碼,但問題是如果遇到一些非常耗時的操作(比如從服務器獲取數據),則會形成阻塞,導致界面假死。另外JavaScript也需要一些定時的處理,比如延遲一段時間后執行一段代碼。比如下圖中韓梅梅要4杯咖啡,耗時4分鐘,導致李雷一直處于等待中,直到5分鐘后才拿到自己需要的咖啡。
所以JavaScript提供了異步模式來解決以上問題,異步模式是由瀏覽器提供了另外的異步調用線程(Web APIs、Event Loop、消息隊列)來協助實現的。就好比增加了一個店小二,并且韓梅梅點完咖啡后就到旁邊等待,然后這個店小二可以馬上為李雷提供服務,李雷在1分鐘后就得到了自己的咖啡。4分鐘后另一個店小二準備好了4杯咖啡,并通知韓梅梅來取。
如下圖所示,當JavaScript發起異步調用后,JavaScript線程和瀏覽器的異步調用線程同時工作,當異步任務完成后,會壓入消息隊列的最后,當JavaScript本輪任務執行結束后(此時調用棧Call stack為空),Event Loop會從消息隊列中依次取出回調任務并執行。
Web APIs:負責定時器的倒計時,倒計時結束后將定時器的回調壓入消息隊列。
Event Loop: 負責監聽調用棧和消息隊列,一旦調用棧清空,則將消息隊列中的任務壓入調用棧。
Queue:存儲等待執行的任務隊列。
宏任務就是消息隊列中的一個個任務,由Event Loop負責監聽并觸發調用。比如由setTimeout開啟的定時器任務就是宏任務。
微任務可以理解為宏任務內部最后被執行的任務。Promise和MutationObserver及Node中的process.nextTick會生成微任務。如下所示代碼,會按數字升序依次打印。
// 宏任務console.log('1')// setTimeout 的回調是 宏任務,進入回調隊列排隊setTimeout(()=>{// 宏任務console.log('4')// setTimeout 的回調是 宏任務2,進入回調隊列排隊setTimeout(()=>{console.log('7')},0)// Promise 的回調是 微任務,本輪調用末尾直接執行Promise.resolve().then(()=>{console.log('6')? ? })console.log('5')},0)// Promise 的回調是 微任務,本輪調用末尾直接執行Promise.resolve().then(()=>{console.log('3')? })console.log('2')
摘自https://www.yuque.com/betteryangjie/ctet6u/zds6xu