JS(瀏覽器)事件環 (宏、微任務)

在說 瀏覽器事件環 以前,先說幾組概念:node

堆(heap)和棧(stack)

堆中存的是引用數據類型,是動態分配的內存,大小不定也不會自動釋放。git

棧中存的是基本數據類型,會自動分配內存空間,自動釋放;ajax

  • 堆(heap):也能夠叫堆內存;是一種隊列優先,先進先出的數據結構;
  • 棧(stack):又名'堆棧',也是一種數據結構,不過它是按照先進後出原則存儲數據的。

給張圖片瞭解一下:數組

圖片加載失敗!

使用JS代碼實現隊列和棧的功能(就是用數組的增刪方法):promise

  • 實現隊列的方法(先進先出)
let arr = new Array();
arr.push(1);
arr.push(2);
arr.shift();
複製代碼
  • 實現棧的方法(先進後出)
let arr = new Array();
arr.push(1);
arr.push(2);
arr.pop();
複製代碼

線程和進程

首先,進程確定要比線程大,一個程序至少要有一個進程,一個進程至少要有一個線程。
下面看一張瀏覽器的工做機制:瀏覽器

圖片加載失敗!

因而可知,瀏覽器就是多進程的,當一個網頁崩潰時不會影響其餘網頁的正常運行。每一個進程管理着瀏覽器不一樣的部分,主要分爲如下幾種:bash

  • 用戶界面:包括地址欄、前進/後退按鈕、書籤菜單等
  • 瀏覽器引擎:在用戶界面和呈現引擎之間傳送指令
  • 呈現引擎,又稱渲染引擎,在線程方面又稱爲UI線程,這是最爲核心的部分,So也被稱之爲瀏覽器內核
  • GPU:用於提升網頁瀏覽的體驗
  • 插件:一個插件對應一個進程(第三方插件進程)

其中渲染引擎內部有三個線程是咱們着重須要關注的網絡

  • Networking:用於網絡調用,好比HTTP請求
  • Javascript解釋器:用於解析和執行Javascript代碼
  • UI Backend

其中js線程和ui線程是互斥的,數據結構

當js執行的時候可能ui還在渲染,那麼這時ui線程會把更改放到隊列中 當js線程空閒下來 ui線程再繼續渲染併發

除此以外還有一些其它的線程,這也是咱們分發異步任務時用到的線程

  • 瀏覽器事件觸發線程
  • 定時觸發器線程
  • 異步HTTP請求線程

說一個老生常談的問題:
JS是單線程的,任務是須要一個一個按順序執行的,可是說的是 JS的主線程是單線程 的,他能夠建立子線程,來幫他完成任務。

同步和異步

同步和異步關注的是消息通知機制

  • 同步在發出調用後,沒有結果前是不返回的,一旦調用返回,就獲得返回值。調用者會主動等待這個調用結果。
  • 異步是發出調用後,調用者不會馬上獲得結果,而是被調用者經過狀態或回調函數來處理這個調用。

任務隊列:

  • 由於JavaScript是單線程的。就意味着全部任務都須要排隊,前一個任務結束,後一個任務才能執行。前一個任務耗時很長,後一個任務也得一直等着。可是IO設備(好比ajax網絡請求)很慢,CPU一直初一顯得狀態,這樣就很不合理了。
  • 因此,其實主線程徹底能夠無論IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回告終果,再回過頭,把掛起的任務繼續執行下去。因而有了同步任務和異步任務。

同步任務 是指在主線程上執行的任務,只有前一個任務執行完畢,下一個任務才能執行。
異步任務 是指不進入主線程,而是進入任務隊列(task queue)的任務,只有主線程任務執行完畢,任務隊列的任務纔會進入主線程執行。

實現過程:
1.全部同步任務都在主線程上執行,造成一個執行棧;
2.只要異步任務有了運行結果,就在任務隊列(task queue)(隊列是一個先進先出的數據結構,而棧是一個先進後出的數據結構)之中放置一個事件;
3.一旦執行棧中的全部同步任務執行完畢,系統就會讀取任務隊列,又將隊列中的事件放到stack中依次執行,就是執行異步任務中的回調函數。這個過程是循環不斷的,這就是Event Loop(事件循環);

關於線程進程,同步異步,想了解更多請參考個人文章《進程與線程、同步與異步、阻塞與非阻塞、併發與並行》

宏任務和微任務

在上面的異步任務中又分爲兩種:宏任務微任務

常見的宏任務和微任務:

  • macro-task(宏任務,優先級低,先定義的先執行): ajax,setTimeout, setInterval, setImmediate, I/O,事件,postMessage,MessageChannel(用於消息通信)

  • micro-task(微任務,優先級高,而且能夠插隊,不是先定義先執行):process.nextTick, 原生 Promise(有些實現的promise將then方法放到了宏任務中),Object.observe(已廢棄), MutationObserver

Promise自己是同步的,Promise.then是異步的

宏任務微任務的區別:微任務是會被加入本輪循環的,而宏任務都是在次輪循環中被執行。簡單就是說,微任務會比宏任務提早執行

簡單的說就是:由於微任務的優先級較高,因此會先將微任務的異步任務取出來進行執行,當微任務的任務都執行完畢以後,會將宏任務中的任務取出來執行。

本輪循環是指什麼呢?JS主線程會從任務隊列中提取任務到執行棧中執行,每一次執行均可能會再產生一個新的任務,對於這些任務來講此次執行到下一次從任務隊列中提取新的任務到執行棧以前就是這些新生任務的本輪。

瀏覽器的事件環(Event Loop)

給出一張網上很火的一張圖:

圖片加載失敗!

從上圖看出:
1.主線程運行的時候產生堆(heap)和棧(stack)
2.棧中的代碼調用各類外部API,它們在"任務隊列"中加入各類事件(例如:click,load,done)
3.只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",將隊列中的事件放到執行棧中依次執行。
4.主線程繼續執行,當再調用外部API時又加入到任務隊列中,等主線程執行完畢又會接着將任務隊列中的事件放到主線程中。
5.上面整個過程是循環不斷的。

例題(執行順序):

JS代碼本質上仍是從上往下執行的

//例題1
console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
複製代碼

輸出結果順序:script start --> script end --> promise1 --> promise2 --> setTimeout

1.先執行同步任務,輸出script star script end
2.而後執行異步任務,先執行異步任務的微任務,輸出 promise1
3.接着返回了一個Promise,而後又.then,仍是微任務,接着執行,輸出 promise2
4.最後執行異步任務中的宏任務,輸出 setTimeout

//例題2
console.log(1);
setTimeout(function(){
    console.log(2);
    new Promise(function(resolve,reject){
        console.log(3);
        resolve();
    }).then(res=>{
        console.log(4);
    })
});
setTimeout(function(){
        console.log(5);
    })
console.log(6);
複製代碼

輸出結果順序:1 6 2 3 4 5

1.執行棧中同步任務先執行,先走console.log(1)console.log(6)
2.接着是遇到setTimeout將它們的回調函數放入MacroTask(宏任務隊列);
3.而後將任務隊列中的回調函數依次放入主執行棧中執行,console.log(2),接着console.log(3);是當即執行,console.log(4);是微任務放入MicroTask中先執行;
4.最後執行第二個setTimeout的回調函數console.log(5)

//例題3
setTimeout(() => {
    console.log('setTimeout1');
    Promise.resolve().then(data => {
        console.log('then3');
    });
},1000);
Promise.resolve().then(data => {
    console.log('then1');
});
Promise.resolve().then(data => {
    console.log('then2');
    setTimeout(() => {
        console.log('setTimeout2');
    },1000);
});
console.log(2);
複製代碼

輸出結果順序:2 then1 then2 setTimeout1 then3 setTimeout2

1.先執行棧中的內容,也就是同步代碼,因此2被輸出出來;
2.而後清空微任務,因此依次輸出的是 then1 then2
3.因代碼是從上到下執行的,因此1s後 setTimeout1 被執行輸出;
4.接着再次清空微任務,then3被輸出;
5.最後執行輸出setTimeout2

下面例題就不一一分析了,能夠本身嘗試運行並分析一下

例題4
setTimeout(() => {
        console.log(2);
        Promise.resolve().then(() => {
            console.log(6);
        });
    }, 0);
    Promise.resolve(3).then((data) => {
        console.log(data);  	
        return data + 1;
    }).then((data) => {
        console.log(data)		
        setTimeout(() => {
            console.log(data + 1)	
            return data + 1;
        }, 1000)
    }).then((data) => {
        console.log(data);		
    });
複製代碼

輸出結果順序:1 3 4 undefined 2 6 5

//例題5
setTimeout(() => {
        console.log('A');
    }, 0);
    var obj = {
        func: function () {
            setTimeout(function () {
                console.log('B')
            }, 0);
            return new Promise(function (resolve) {
                console.log('C');
                resolve();
            })
        }
    };
    obj.func().then(function () {
        console.log('D')
    });
    console.log('E'); 
複製代碼

輸出結果順序:C E D A B

擴充

這裏內容不作過多解釋

1. setTimeout,setImmediate誰先誰後?

  • 若是二者都在主模塊中調用,那麼執行前後取決於進程性能,也就是隨機。
  • 若是二者都不在主模塊調用(被一個異步操做包裹),那麼setImmediate的回調永遠先執行。

2. nextTick和promise.then誰快?

  • nextTick快,就是這麼設計的

3. nextTick和其它的定時器嵌套

setImmediate(function(){
  console.log(1);
  process.nextTick(function(){
    console.log(4);
  })
})
process.nextTick(function(){
  console.log(2);
  setImmediate(function(){
    console.log(3);
  })
})


2 1 3 4
複製代碼

緣由在於nextTick在node中的執行實際和瀏覽器中不徹底同樣,雖然它們在第一次進入事件環時都會先執行,但若是後續還有nextTick加入,node中只會在階段轉換時纔會去執行,而瀏覽器中則是一有nextTick加入就會當即執行。

形成這樣區別的緣由在於,node中的事件環是有6種狀態的,每種狀態都是一個callbcak queue,只有當一個狀態的callback queue中存放的回調都清空後纔會執行nextTick

4. 定時器指定的回調函數必定會在指定的時間內執行嗎?

不必定,先不說node中事件環的六種狀態之間轉化時的貓膩,光是瀏覽器中的事件環也可能由於本輪循環的執行時間過長,長的比定時器指定的事件還長從而致使定時器的回調觸發被延誤。


node11.x版本以後,node事件環就慢慢和瀏覽器的事件環同樣了,這裏就先不做過多解釋


^_<

相關文章
相關標籤/搜索