宏任務 和 微任務

宏任務:

當前調用棧執行的代碼成為宏任務,(主代碼塊和定時器)也或者宿主環境提供的叫宏任務

這些任務包括:

  • 渲染事件
  • 用戶交互事件(如鼠標點擊、滾動頁面、放大縮小等)
  • JavaScript 腳本執行事件;
  • 網絡請求完成、文件讀寫完成事件

微任務:

當前(此次事件循環中)宏任務執行完,在下一個宏任務開始之前需要執行的任務,可以理解為回調事件:promise.then,proness.nextTick等等。 由語言標準提供的叫微任務.

執行順序:

在掛起任務的時候, JS 引擎會把任務按照類別分到兩個隊伍當中。 首先在宏任務(macrotask) 隊伍中取出第一個任務,執行完畢后。取出 microtask 隊列中的所有任務順序執行;周而復始,循環。

總結上面:
可以舉個例子:就像銀行柜臺辦理業務,每個來辦理業務的人就像一個一個宏任務,當前用戶業務辦理完成然后 接待下一個客戶,就像開始了下一個宏任務一樣。但是一個客戶可能要辦理多項業務(修改密碼,存款,轉賬等)這些業務就像微任務,只有微任務執行完成,才能執行下一個宏任務(總不能一個客戶業務沒有辦理完,就讓他去重新取號排隊,估計要打人了!?。。?/p>

setTimeout(function(){
    console.log('定時器開始啦')
});

new Promise(function(resolve){
    console.log('馬上執行for循環啦');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('執行then函數啦')
});

console.log('代碼執行結束');

//馬上執行for循環啦
//代碼執行結束
//執行then函數啦
//定時器開始啦
  • 這段代碼作為宏任務,進入主線程。
  • 先遇到setTimeout,那么將其回調函數注冊后分發到宏任務Event Queue。(注冊過程與上同,下文不再描述)
  • 接下來遇到了Promise,new Promise立即執行,then函數分發到微任務Event Queue。
  • 遇到console.log(),立即執行。
  • 好啦,整體代碼script作為第一個宏任務執行結束,看看有哪些微任務?我們發現了then在微任務Event Queue里面,執行。
  • ok,第一輪事件循環結束了,我們開始第二輪循環,當然要從宏任務Event Queue開始。我們發現了宏任務Event Queue中setTimeout對應的回調函數,立即執行。
  • 結束

看下demo:

Promise 在前 setTimeout在后面

new Promise((resolve) => {
  console.log('外層宏事件2');
  resolve()
}).then(() => {
  console.log('微事件1');
}).then(()=>{
  console.log('微事件2')
})
console.log('外層宏事件1');
setTimeout(() => {
  //執行后 回調一個宏事件
  console.log('內層宏事件3')
}, 0)

// 執行結果:
外層宏事件2
外層宏事件1
微事件1
微事件2
內層宏事件3
  • Promise 在前,Promise.then則是具有代表性的微任務,所有會進入的異步都是指事件的回調。所以說 new Promise 在實例化的過程中所執行的代碼都是同步進行的,而then 中的才是異步執行的
  • setTimeout就是作為宏任務來存在的
  • 在同步執行完成之后,檢查是否有異步任務,微任務會在下一個宏任務前面全部完成
  • 所以 結果是 外2-外1-微1-微2-內3

setTimeout 在前面 Promise 在后面

setTimeout(() => {
  //執行后 回調一個宏事件
  console.log('內層宏事件3')
}, 0)
console.log('外層宏事件1');

new Promise((resolve) => {
  console.log('外層宏事件2');
  resolve()
}).then(() => {
  console.log('微事件1');
}).then(()=>{
  console.log('微事件2')
})
// 執行結果:
外層宏事件1
外層宏事件2
微事件1
微事件2
內層宏事件3
  • setTimeout 設定了時間,相當于取號了,在排隊過程。
  • 然后在當前進程中又添加了一些Promise的處理(臨時添加的業務)
  • 同步的外1 和外2 執行完成,開始執行微任務,微任務執行完成之后才執行下一個異步宏任務,所以結果如上;

前面提到宿主環境:能夠使js 完美運行的環境,目前常見的環境就是兩種宿主環境有瀏覽器和node

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
// 1-7 -6 -8 -2- 4- 3- 5- 9 -11- 10 -12

  • 整體script作為第一個宏任務進入主線程,遇到console.log,輸出1。
  • 遇到setTimeout,其回調函數被分發到宏任務Event Queue中。我們暫且記為setTimeout1。
  • 遇到process.nextTick(),其回調函數被分發到微任務Event Queue中。我們記為process1。
  • 遇到Promise,new Promise直接執行,輸出7。then被分發到微任務Event Queue中。我們記為then1。
  • 又遇到了setTimeout,其回調函數被分發到宏任務Event Queue中,我們記為setTimeout2。
  • 同步輸出1-7 執行微任務 process1-then1 輸出6 - 8
  • 好了,第一輪事件循環正式結束,這一輪的結果是輸出1,7,6,8。那么第二輪時間循環從setTimeout1宏任務開始:
    • 首先輸出2。接下來遇到了process.nextTick(),同樣將其分發到微任務Event Queue中,記為process2。new Promise立即執行輸出4,then也分發到微任務Event Queue中,記為then2。
    • 第二輪事件循環宏任務結束,我們發現有process2和then2兩個微任務可以執行。 輸出 3和5
    • 第二輪事件循環結束,第二輪輸出2,4,3,5。
    • 第三輪事件循環開始,此時只剩setTimeout2了,執行。
    • 輸出9,11,10,12。

node 環境和瀏覽器環境 又有什么區別呢?

  • 宏任務


    WechatIMG506.png
  • requestAnimationFrame姑且也算是宏任務吧,requestAnimationFrameMDN的定義為,下次頁面重繪前所執行的操作,而重繪也是作為宏任務的一個步驟來存在的,且該步驟晚于微任務的執行

  • 微任務

WechatIMG507.png

JS 是單線程,所以同一個時間不能處理多個任務,所以每次辦理完一個業務,都會詢問當前客戶是否還有其他要辦理的業務(檢查是否有未完成的微任務),當前用戶辦理完成,結束這個宏任務開始下一個宏任務,這樣操作持續進行,而這樣的操作就被稱為Event Loop

什么是Event Loop?

event loop 顧名思義 就是事件循環。因為V8是單線程的,即同一時間只能干一件事情,但是呢文件的讀取,網絡的IO處理是很緩慢的,并且是不確定的,如果同步等待它們響應,那么用戶就起飛了。于是我們就把這個事件加入到一個 事件隊列里(task),等到事件完成時,event loop再執行另一個事件隊列。

1、 update_time
在事件循環的開頭,這一步的作用實際上是為了獲取一下系統時間

2、timers
事件循環跑到這個階段的時候,要檢查是否有到期的timer,其實也就是setTimeout和setInterval這種類型的timer,到期了,就會執行他們的回調。

3、I/O callbacks
處理異步事件的回調,比如網絡I/O,比如文件讀取I/O。當這些I/O動作都結束的時候,在這個階段會觸發它們的回調。

4、idle, prepare
這個階段內部做一些動作,與理解事件循環沒啥關系

5、I/O poll階段
這個階段相當有意思,也是事件循環設計的一個有趣的點。這個階段是選擇運行的。選擇運行的意思就是不一定會運行。

6、check
執行setImmediate操作

7、close callbacks
關閉I/O的動作,比如文件描述符的關閉,鏈接斷開

除了task還有一個microtask,這一個概念是ES6提出Promise以后出現的。這個microtask queue只有一個。
并且會在且一定會在每一個task后執行,且執行是按順序的。加入到microtask 的事件類型有Promise.resolve().then(), process.nextTick() 值得注意的是
event loop一定會在執行完micrtask以后才會尋找新的 可執行的task隊列。而microtask事件內部又可以產生新的microtask

瀏覽器:

  • 宏任務(macroTask):script 中代碼、setTimeout、setInterval、I/O、UI render
  • 微任務(microTask): Promise、Object.observe、MutationObserver。
  • I/O 有點籠統,點擊個btn 上傳一個文件,與程序交互的這些都可以稱為I/O

<style>
  #outer {
    padding: 20px;
    background: #616161;
  }

  #inner {
    width: 100px;
    height: 100px;
    background: #757575;
  }
</style>
<div id="outer">
  <div id="inner"></div>
</div>


const $inner = document.querySelector('#inner')
const $outer = document.querySelector('#outer')

function handler () {
  console.log('click') // 直接輸出

  Promise.resolve().then(_ => console.log('promise')) // 注冊微任務

  setTimeout(_ => console.log('timeout')) // 注冊宏任務

  requestAnimationFrame(_ => console.log('animationFrame')) // 注冊宏任務

  $outer.setAttribute('data-random', Math.random()) // DOM屬性修改,觸發微任務
}

new MutationObserver(_ => {
  console.log('observer')
}).observe($outer, {
  attributes: true
})

$inner.addEventListener('click', handler)
$outer.addEventListener('click', handler)

1、因為一次I/O創建了一個宏任務,也就是說在這次任務中會去觸發handler
2、在同步的代碼已經執行完以后,這時就會去查看是否有微任務可以執行,然后發現了Promise和MutationObserver兩個微任務,遂執行之
3、click事件會冒泡,所以對應的這次I/O會觸發兩次handler函數(一次在inner、一次在outer),所以會優先執行冒泡的事件(早于其他的宏任務),也就是說會重復上述的邏輯
4、在執行完同步代碼與微任務以后,這時繼續向后查找有木有宏任務
5、因為我們觸發了setAttribute,實際上修改了DOM的屬性,這會導致頁面的重繪,而這個set的操作是同步執行的,也就是說requestAnimationFrame的回調會早于setTimeout所執行

所以上面 執行順序是:click -> promise -> observer -> click -> promise -> observer -> animationFrame -> animationFrame -> timeout -> timeout

node:

  • Node也是單線程,但是在處理Event Loop上與瀏覽器稍微有些不同
setImmediate與setTimeout的區別:

在官方文檔中的定義,setImmediate為一次Event Loop執行完畢后調用。
setTimeout則是通過計算一個延遲時間后進行執行。

  • microTask:微任務;

  • nextTick:process.nextTick;

  • timers:執行滿足條件的 setTimeout 、setInterval 回調;

  • I/O callbacks:是否有已完成的 I/O 操作的回調函數,來自上一輪的 poll 殘留;

  • poll:等待還沒完成的 I/O 事件,會因 timers 和超時時間等結束等待;

  • check:執行 setImmediate 的回調;

  • close callbacks:關閉所有的 closing handles ,一些 onclose 事件;
    idle/prepare 等等:可忽略。


  • macro-task(宏任務):包括整體代碼script,setTimeout,setInterval
  • micro-task(微任務):Promise,process.nextTick

進程和線程的區別:

  • 線程是程序執行的最小單位,而進程是操作系統分配資源的最小單位;
  • 一個進程由一個或多個線程組成,線程是一個進程中代碼的不同執行路線
  • 進程之間相互獨立,但同一進程下的各個線程之間共享程序的內存空間(包括代碼段,數據集,堆等)及一些進程級的資源(如打開文件和信號等),某進程內的線程在其他進程不可見;
  • 調度和切換:線程上下文切換比進程上下文切換要快得多
  • 進程是操作系統資源分配的基本單位,而線程是任務調度和執行的基本單位
  • 內存分配方面:
    系統在運行的時候會為每個進程分配不同的內存空間;
    而對線程而言,除了CPU外,系統不會為線程分配內存(線程所使用的資源來自其所屬進程的資源),線程組之間只能共享資源。
  • 所處環境:在操作系統中能同時運行多個進程(程序);而在同一個進程(程序)中有多個線程同時執行(通過CPU調度,在每個時間片中只有一個線程執行)
  • 創建一個線程比進程開銷??;
  • 線程之間通信更方便,同一個進程下,線程共享全局變量,靜態變量等數據,進程之間的通信需要以通信的方式(IPC)進行;(但多線程程序處理好同步與互斥是個難點)

瀏覽器都有哪些進程?

1.Browser進程(即上篇文章截圖里面的瀏覽器進程):瀏覽器的主進程(負責協調、主控),只有一個。主要作用:

  • 負責瀏覽器界面顯示,與用戶交互。如前進,后退等
  • 負責各個頁面的管理,創建和銷毀其他進程
  • 將渲染(Renderer)進程得到的內存中的Bitmap(位圖),繪制到用戶界面上
  • 網絡資源的管理,下載等

2、第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
3、GPU進程:最多一個,用于3D繪制等
4、瀏覽器渲染進程(即通常所說的瀏覽器內核)(Renderer進程,內部是多線程的):主要作用為頁面渲染,腳本執行,事件處理等

瀏覽器內核

簡單來說瀏覽器內核是通過取得頁面內容、整理信息(應用CSS)、計算和組合最終輸出可視化的圖像結果,通常也被稱為渲染引擎。從上面我們可以知道,Chrome瀏覽器為每個tab頁面單獨啟用進程,因此每個tab網頁都有由其獨立的渲染引擎實例

瀏覽器內核是多線程,在內核控制下各線程相互配合以保持同步,一個瀏覽器通常由以下常駐線程組成

  • GUI 渲染線程

GUI渲染線程負責渲染瀏覽器界面HTML元素,當界面需要重繪(Repaint)或由于某種操作引發回流(reflow)時,該線程就會執行。在Javascript引擎運行腳本期間,GUI渲染線程都是處于掛起狀態的,也就是說被”凍結”了.

  • JavaScript引擎線程

Javascript引擎,也可以稱為JS內核,主要負責處理Javascript腳本程序,例如V8引擎。Javascript引擎線程理所當然是負責解析Javascript腳本

  • 定時觸發器線程
  • 事件觸發線程
  • 異步http請求線程
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容