JS 運行機制-EventLoop(事件循環)

JS 運行機制-EventLoop(事件循環)

javascript 是單線程的

JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那麼,爲什麼JavaScript不能有多個線程呢?這樣能提高效率啊。

JavaScript的單線程,與它的用途有關。作爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程爲準?

所以,爲了避免複雜性,從一誕生,JavaScript就是單線程。

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

主線程和任務隊列

單線程就意味着,所有任務需要排隊。所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。

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

異步任務:不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。

具體來說:

 1. 所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
 2. 主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件
 3. 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
 4. 主線程不斷重複上面的第三步。

下圖就是主線程和任務隊列的示意圖
在這裏插入圖片描述
主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱爲Event Loop(事件循環)。
只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制

宏任務和微任務

瞭解完主線程,還要了解一下任務,任務有宏任務(MacroTask)和微任務(MicroTask)之分。

macro-task(宏任務):可以理解爲每次執行棧執行的代碼就是一個宏任務,包括每次從任務隊列中獲取一個事件並將其對應的回調放入到執行棧中執行。宏任務需要多次事件循環才能執行完,任務隊列中的每一個事件都是一個宏任務。每次事件循環都會調入一個宏任務,瀏覽器爲了能夠使 JS 內部宏任務與 DOM 任務有序的執行,會在一個宏任務結束後,下一個宏任務開始前,對頁面進行重新渲染

micro-task(微任務):可以理解爲在當前宏任務執行結束後立即執行的任務。微任務是一次性執行完的,在一個宏任務執行完畢後,就會將它執行期間產生的所有微任務都執行完畢。如果在微任務執行期間微任務隊列加入了新的微任務,會將新的微任務放到隊列尾部,之後會依次執行。

宏任務主要有:script代碼段、setTimeout、setInterval、Promise的構造函數、I/O、setImmediate(node)等.

微任務主要有:process.nextTick(node),Promise的回調(Promist.then、catch、finally)、MutationObserver

在這裏插入圖片描述

好了概念講清楚了,下面看幾個例子

宏任務

瀏覽器爲了能夠使 宏任務和 DOM任務有序的進行,會在一個 宏任務執行結果後,在下一個 宏任務執行前, GUI渲染線程開始工作,對頁面進行渲染。

document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';

我們可以將這段代碼放到瀏覽器的控制檯執行以下,看一下效果:
在這裏插入圖片描述
我們會看到的結果是,頁面背景會在瞬間變成白色,以上代碼屬於同一次 宏任務,所以全部執行完才觸發 頁面渲染,渲染時 GUI線程會將所有UI改動優化合並,所以視覺效果上,只會看到頁面變成灰色。

第二個例子:

document.body.style = 'background:blue';
setTimeout(function(){
	document.body.style = 'background:black';
})

執行一下,再看效果:
在這裏插入圖片描述
我會看到,頁面先顯示成藍色背景,然後瞬間變成了黑色背景,這是因爲以上代碼屬於兩次 宏任務,第一次 宏任務執行的代碼是將背景變成藍色,然後觸發渲染,將頁面變成藍色,再觸發第二次宏任務將背景變成黑色。

微任務
document.body.style = 'background:blue';
console.log(1);
Promise.resolve().then()=>{
	console.log(2);
	document.body.style = 'background:black';
}
console.log(3)

執行一下,再看效果:
在這裏插入圖片描述
控制檯輸出 1 3 2 , 是因爲 promise 對象的 then 方法的回調函數是異步執行,所以 2 最後輸出

頁面的背景色直接變成黑色,沒有經過藍色的階段,是因爲,我們在宏任務中將背景設置爲藍色,但在進行渲染前執行了微任務,
在微任務中將背景變成了黑色,然後才執行的渲染

第二個例子:

setTimeout(()=>{
	console.log(1);
	Promise.resolve(3).then(data=> console.log(data));
},0)
setTimeout(() => {
	console.log(2);
},0)

上面代碼共包含兩個 setTimeout ,也就是說除主代碼塊外,共有兩個 宏任務,
其中第一個 宏任務執行中,輸出 1 ,並且創建了 微任務隊列,所以在下一個 宏任務隊列執行前,
先執行 微任務,在 微任務執行中,輸出 3 ,微任務執行後,執行下一次 宏任務,執行中輸出 2

總結:

執行一個 宏任務(棧中沒有就從 事件隊列中獲取)

執行過程中如果遇到 微任務,就將它添加到 微任務的任務隊列中

宏任務執行完畢後,立即執行當前 微任務隊列中的所有 微任務(依次執行)

當前 宏任務執行完畢,開始檢查渲染,然後 GUI線程接管渲染

渲染完畢後, JS線程繼續接管,開始下一個 宏任務(從事件隊列中獲取)

async/await

關於async/await 相關優點 :Async/Await替代Promise的6個理由
async
當我們在函數前使用async的時候,使得該函數返回的是一個Promise對象

async function test() {
    return 1   // async的函數會在這裏幫我們隱士使用Promise.resolve(1)
}
// 等價於下面的代碼
function test() {
   return new Promise(function(resolve, reject) {
       resolve(1)
   })
}

可見async只是一個語法糖,只是幫助我們返回一個Promise而已

await
await表示等待,是右側「表達式」的結果,這個表達式的計算結果可以是 Promise 對象的值或者一個函數的值(換句話說,就是沒有特殊限定)。並且只能在帶有async的內部使用

使用await時,會從右往左執行,當遇到await時,會阻塞函數內部處於它後面的代碼,去執行該函數外部的同步代碼,當外部同步代碼執行完畢,再回到該函數內部執行剩餘的代碼, 並且當await執行完畢之後,會先處理微任務隊列的代碼
下面來看一個栗子:

async function async1() {
    console.log( 'async1 start' )
    await async2()
    console.log( 'async1 end' )
}
async function async2() {
    console.log( 'async2' )
}
console.log( 'script start' )
setTimeout( function () {
    console.log( 'setTimeout' )
}, 0 )
async1();
new Promise( function ( resolve ) {
    console.log( 'promise1' )
    resolve();
} ).then( function () {
    console.log( 'promise2' )
} )
console.log( 'script end' )      

下面是在chrome瀏覽器上輸出的結果
在這裏插入圖片描述
使用事件循環機制分析:

  1. 首先執行同步代碼,console.log( ‘script start’ )
  2. 遇到setTimeout,會被推入宏任務隊列
  3. 執行async1(), 它也是同步的,只是返回值是Promise,在內部首先執行console.log( ‘async1 start’ )
  4. 然後執行async2(), 然後會打印console.log( ‘async2’ )
  5. 從右到左會執行, 當遇到await的時候,阻塞後面的代碼,去外部執行同步代碼
  6. 進入 new Promise,打印console.log( ‘promise1’ )
  7. 將.then放入事件循環的微任務隊列
  8. 繼續執行,打印console.log( ‘script end’ )
  9. 外部同步代碼執行完畢,接着回到async1()內部, 由於async2()其實是返回一個Promise, await async2()相當於獲取它的值,其實就相當於這段代碼Promise.resolve(undefined).then((undefined) => {}),所以.then會被推入微任務隊列, 所以現在微任務隊列會有兩個任務。接下來處理微任務隊列,打印console.log( ‘promise2’ ),後面一個.then不會有任何打印,但是會執行
  10. 行後面的代碼, 打印console.log( ‘async1 end’ )
  11. 進入第二次事件循環,執行宏任務隊列, 打印console.log( ‘setTimeout’ )

粗略大概,規範在改,promise, async await 比想象的還要複雜些

來幾個常見的題目吧

console.log(1);
while(true){};
cosole.log(2);
// 1

結果:因爲這是同步任務,程序由上到下執行,遇到while()死循環,下面語句就沒辦法執行

 console.log(1);
 setTimeout(()=>{
 	console.log(2);
 })
 while(true){};
 // 1

依然是 1,因爲setTimeout()就是個異步任務

console.log(1);
    
 setTimeout(() => {
   console.log('setTimeout');
 }, 0);

 let promise = new Promise(resolve => {
   console.log(3);
   resolve();
 }).then(data => {
   console.log(100);
 }).then(data => {
   console.log(200);
 });
    
 console.log(2);
有好玩的我就加一下
const first = () => (new Promise((resolve,reject)=>{
    console.log(3);
    let p = new Promise((resolve, reject)=>{
         console.log(7);
        setTimeout(()=>{
           console.log(5);
           resolve(6); 
        },0)
        resolve(1);
    }); 
    resolve(2);
    p.then((arg)=>{
        console.log(arg);
    });

}));

first().then((arg)=>{
    console.log(arg);
});
console.log(4);

第一輪循環
先執行宏任務,主script ,new Promise立即執行,輸出【3】,執行p這個new Promise 操作,輸出【7】,發現setTimeout,將回調放入下一輪任務隊列(Event Queue),p的then,姑且叫做then1,放入微任務隊列,發現first的then,叫then2,放入微任務隊列。執行console.log(4),輸出【4】,宏任務執行結束。
再執行微任務,執行then1,輸出【1】,執行then2,輸出【2】。到此爲止,第一輪事件循環結束。開始執行第二輪。

第二輪循環
先執行宏任務裏面的,也就是setTimeout的回調,輸出【5】。resovle不會生效,因爲p這個Promise的狀態一旦改變就不會在改變了。 所以最終的輸出順序是3、7、4、1、2、5。

掘金上有個題不錯
https://juejin.im/post/5a04066351882517c416715d

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章