js 異步執(zhí)行順序

js的執(zhí)行順序,先同步后異步
異步中任務(wù)隊(duì)列的執(zhí)行順序: 先微任務(wù)microtask隊(duì)列,再宏任務(wù)macrotask隊(duì)列
調(diào)用Promise 中的resolve,reject屬于微任務(wù)隊(duì)列,setTimeout屬于宏任務(wù)隊(duì)列
注意以上都是 隊(duì)列,先進(jìn)先出。
微任務(wù)包括 process.nextTick ,promise ,MutationObserver。
宏任務(wù)包括 scriptsetTimeout ,setIntervalsetImmediate ,I/O ,UI rendering。

事件循環(huán)是什么

首先,JavaScript是一門單線程的語(yǔ)言,意味著同一時(shí)間內(nèi)只能做一件事,但是這并不意味著單線程就是阻塞,而實(shí)現(xiàn)單線程非阻塞的方法就是事件循環(huán)

在JavaScript中,所有的任務(wù)都可以分為

  • 同步任務(wù):立即執(zhí)行的任務(wù),同步任務(wù)一般會(huì)直接進(jìn)入到主線程中執(zhí)行
  • 異步任務(wù):異步執(zhí)行的任務(wù),比如ajax網(wǎng)絡(luò)請(qǐng)求,setTimeout定時(shí)函數(shù)等

同步任務(wù)與異步任務(wù)的運(yùn)行流程圖如下:

image.png

從上面我們可以看到,同步任務(wù)進(jìn)入主線程,即主執(zhí)行棧,異步任務(wù)進(jìn)入任務(wù)隊(duì)列,主線程內(nèi)的任務(wù)執(zhí)行完畢為空,會(huì)去任務(wù)隊(duì)列讀取對(duì)應(yīng)的任務(wù),推入主線程執(zhí)行。上述過(guò)程的不斷重復(fù)就事件循環(huán)

調(diào)用棧(Call Stack)

是一種后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)。當(dāng)一個(gè)腳本執(zhí)行的時(shí)候,js引擎會(huì)解析這段代碼,并將其中的同步代碼按照?qǐng)?zhí)行順序加入調(diào)用棧中,然后從頭開始執(zhí)行。

事件隊(duì)列 (Task Queue)

js引擎遇到一個(gè)異步任務(wù)后并不會(huì)一直等待其返回結(jié)果,而是會(huì)將這個(gè)任務(wù)交給瀏覽器的其他模塊進(jìn)行處理(以webkit為例,是webcore模塊),繼續(xù)執(zhí)行調(diào)用棧中的其他任務(wù)。當(dāng)一個(gè)異步任務(wù)返回結(jié)果后,js引擎會(huì)將這個(gè)任務(wù)加入與當(dāng)前調(diào)用棧不同的另一個(gè)隊(duì)列,我們稱之為事件隊(duì)列。

當(dāng)一個(gè)腳本執(zhí)行的時(shí)候,js引擎會(huì)解析這段代碼,并將其中的同步代碼按照?qǐng)?zhí)行順序加入調(diào)用棧中,然后從頭開始執(zhí)行。

js引擎遇到一個(gè)異步事件后并不會(huì)一直等待其返回結(jié)果,而是會(huì)將這個(gè)事件掛起(其他模塊進(jìn)行處理),繼續(xù)執(zhí)行執(zhí)行棧中的其他任務(wù)。當(dāng)一個(gè)異步事件返回結(jié)果后,js會(huì)將這個(gè)事件加入到事件隊(duì)列。

被放入事件隊(duì)列不會(huì)立刻執(zhí)行其回調(diào),而是等待當(dāng)前執(zhí)行棧中的所有任務(wù)都執(zhí)行完畢, 主線程處于閑置狀態(tài)時(shí),主線程會(huì)去查找事件隊(duì)列是否有任務(wù)。如果有,那么主線程會(huì)從中取出排在第一位的事件,并把這個(gè)事件對(duì)應(yīng)的回調(diào)放入執(zhí)行棧中,然后執(zhí)行其中的同步代碼…,如此反復(fù),這樣就形成了一個(gè)無(wú)限的循環(huán)。這個(gè)過(guò)程被稱為“事件循環(huán)(Event Loop)”。

同步任務(wù)

首先,我們用一個(gè)棧來(lái)表示主線程

image.png

當(dāng)有多個(gè)同步任務(wù)時(shí),這些同步任務(wù)會(huì)依次入棧出棧,如下圖

image.png

同步任務(wù)1先入棧,執(zhí)行完之后,出棧,接著同步任務(wù)2入棧,依此類推

這只是同步任務(wù)的執(zhí)行方式,那么異步任務(wù)呢?

異步任務(wù)

異步任務(wù)會(huì)在同步任務(wù)執(zhí)行之后再去執(zhí)行,那么如果異步任務(wù)代碼在同步任務(wù)代碼之前呢?在js機(jī)制里,存在一個(gè)隊(duì)列,叫做任務(wù)隊(duì)列,專門用來(lái)存放異步任務(wù)。也就是說(shuō),當(dāng)異步任務(wù)出現(xiàn)的時(shí)候,會(huì)先將異步任務(wù)存放在任務(wù)隊(duì)列中,當(dāng)執(zhí)行完所有的同步任務(wù)之后,再去調(diào)用任務(wù)隊(duì)列中的異步任務(wù)

例如下圖,現(xiàn)在存在兩個(gè)同步任務(wù),兩個(gè)異步任務(wù)

image.png

js會(huì)先將同步任務(wù)1提至主線程,然后發(fā)現(xiàn)異步任務(wù)1和2,則將異步任務(wù)1和2依次放入任務(wù)隊(duì)列

image.png

異步任務(wù)分類

js中,又將異步任務(wù)分為宏任務(wù)和微任務(wù),所以,上述任務(wù)隊(duì)列也分為宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列,那么,什么是宏任務(wù),什么是微任務(wù)呢?

I/O、定時(shí)器、事件綁定、ajax等都是宏任務(wù)

Promise的then、catch、finally和process的nextTick都是微任務(wù)

注意:Promise的then等方法是微任務(wù),而Promise中的代碼是同步任務(wù),并且,nextTick的執(zhí)行順序優(yōu)先于Promise的then等方法,因?yàn)閚extTick是直接告訴瀏覽器說(shuō)要盡快執(zhí)行,而不是放入隊(duì)列

js中,微任務(wù)總是先于宏任務(wù)執(zhí)行,也就是說(shuō),這三種任務(wù)的執(zhí)行順序是:同步任務(wù)>微任務(wù)>宏任務(wù)

宏任務(wù)與微任務(wù)

如果將任務(wù)劃分為同步任務(wù)和異步任務(wù)并不是那么的準(zhǔn)確,舉個(gè)例子:

console.log(1)
setTimeout(()=>{    
  console.log(2)}, 0)
new Promise((resolve, reject)=>{    
    console.log('new Promise')    
    resolve()
  }).then(()=>{    
    console.log('then')
})
console.log(3)

如果按照上面流程圖來(lái)分析代碼,我們會(huì)得到下面的執(zhí)行步驟:

  • console.log(1),同步任務(wù),主線程中執(zhí)行
  • setTimeout() ,異步任務(wù),放到 Event Table,0 毫秒后console.log(2)回調(diào)推入 Event Queue 中
  • new Promise ,同步任務(wù),主線程直接執(zhí)行
  • .then ,異步任務(wù),放到 Event Table
  • console.log(3),同步任務(wù),主線程執(zhí)行

所以按照分析,它的結(jié)果應(yīng)該是 1 => 'new Promise' => 3 => 2 => 'then'

但是實(shí)際結(jié)果是:1=>'new Promise'=> 3 => 'then' => 2

出現(xiàn)分歧的原因在于異步任務(wù)執(zhí)行順序,事件隊(duì)列其實(shí)是一個(gè)“先進(jìn)先出”的數(shù)據(jù)結(jié)構(gòu),排在前面的事件會(huì)優(yōu)先被主線程讀取

例子中 setTimeout回調(diào)事件是先進(jìn)入隊(duì)列中的,按理說(shuō)應(yīng)該先于 .then 中的執(zhí)行,但是結(jié)果卻偏偏相反

原因在于異步任務(wù)還可以細(xì)分為微任務(wù)與宏任務(wù)

微任務(wù)

一個(gè)需要異步執(zhí)行的函數(shù),執(zhí)行時(shí)機(jī)是在主函數(shù)執(zhí)行結(jié)束之后、當(dāng)前宏任務(wù)結(jié)束之前

常見的微任務(wù)有:

  • Promise.then
  • MutaionObserver
  • Object.observe(已廢棄;Proxy 對(duì)象替代)
  • process.nextTick(Node.js)

宏任務(wù)

宏任務(wù)的時(shí)間粒度比較大,執(zhí)行的時(shí)間間隔是不能精確控制的,對(duì)一些高實(shí)時(shí)性的需求就不太符合

常見的宏任務(wù)有:

  • script (可以理解為外層同步代碼)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

這時(shí)候,事件循環(huán),宏任務(wù),微任務(wù)的關(guān)系如圖所示

image.png

按照這個(gè)流程,它的執(zhí)行機(jī)制是:

  • 執(zhí)行一個(gè)宏任務(wù),如果遇到微任務(wù)就將它放到微任務(wù)的事件隊(duì)列中
  • 當(dāng)前宏任務(wù)執(zhí)行完成后,會(huì)查看微任務(wù)的事件隊(duì)列,然后將里面的所有微任務(wù)依次執(zhí)行完

回到上面的題目

console.log(1)
setTimeout(()=>{    
  console.log(2)}, 0)
new Promise((resolve, reject)=>{    
    console.log('new Promise')    
    resolve()
  }).then(()=>{    
    console.log('then')
})
console.log(3)

流程如下

遇到 console.log(1) ,直接打印 1
遇到定時(shí)器,屬于新的宏任務(wù),留著后面執(zhí)行
遇到 new Promise,這個(gè)是直接執(zhí)行的,打印 'new Promise'
.then 屬于微任務(wù),放入微任務(wù)隊(duì)列,后面再執(zhí)行
遇到 console.log(3) 直接打印 3
好了本輪宏任務(wù)執(zhí)行完畢,現(xiàn)在去微任務(wù)列表查看是否有微任務(wù),發(fā)現(xiàn) .then 的回調(diào),執(zhí)行它,打印 'then'
當(dāng)一次宏任務(wù)執(zhí)行完,再去執(zhí)行新的宏任務(wù),這里就剩一個(gè)定時(shí)器的宏任務(wù)了,執(zhí)行它,打印 2

async與await

async 是異步的意思,await則可以理解為 async wait。所以可以理解async就是用來(lái)聲明一個(gè)異步方法,而 await是用來(lái)等待異步方法執(zhí)行

async和await是es7提供的語(yǔ)法,相比于es6的promise ,具有更高的代碼可讀性

從字面意思理解async是異步的意思,await是等待的意思,那么他們的作用就很容易看出了:

async : 聲明一個(gè)函數(shù)是異步的

await : 等待一個(gè)異步函數(shù)執(zhí)行完成

語(yǔ)法注意:await必須聲明在async內(nèi)部,因?yàn)閍sync會(huì)阻斷后邊代碼的執(zhí)行,說(shuō)到阻斷大家不要慌,因?yàn)檫@里的阻斷都是在一個(gè)async聲明的promise函數(shù)里的阻斷,不會(huì)影響整體代碼的執(zhí)行,async外邊的代碼還是照常執(zhí)行

async語(yǔ)法的優(yōu)勢(shì)

async語(yǔ)法在處理then鏈的時(shí)候有優(yōu)勢(shì),我們?nèi)绻胮romise的then嵌套可能會(huì)寫出多個(gè)then鏈,雖然解決了回調(diào)地獄的問題,但是感覺好亂啊

用async和await實(shí)現(xiàn)多接口調(diào)用的代碼:

function callServe1(){
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            resolve({result:"callserve1"})
        }, 1000);
    })}
function callServe2(res){
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            resolve(res)
        }, 1000);
    })}
function callServe3(res){
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            resolve(res)
        }, 1000);
    })}
async function getAll(){
    const result1 = await callServe1()
    const result2 = await callServe2(result1)
    const result3 = await callServe3(result2)
    console.log(result3) //{ result: 'callserve1' }}
getAll()

async

1.函數(shù)前面加上 async 關(guān)鍵字,則該函數(shù)會(huì)返回一個(gè)結(jié)果為 promise 的對(duì)象。

2. async 函數(shù)返回 promise 對(duì)象的狀態(tài)。

2.1:如果返回的是一個(gè)非 Promise 類型的數(shù)據(jù), 則async 函數(shù)返回 promise 的狀態(tài) 為 fulfilled 成功。

2.2:如果返回的是一個(gè) Promise對(duì)象,則 async 函數(shù)返回 promise 的狀態(tài)由返回的Promise對(duì)象的狀態(tài)決定。

2.3:如果 throw Errow 拋出異常,則 async 函數(shù)返回 promise 的狀態(tài)為 rejected 失敗。

async函數(shù)返回一個(gè)promise對(duì)象,下面兩種方法是等效的

function f() {
    return Promise.resolve('TEST');
}
async function asyncF() {
    return 'TEST';
}

await

1.await 右側(cè)的表達(dá)式一般為 promise 對(duì)象。

2.await 是在等一個(gè)表達(dá)式的結(jié)果,等一個(gè)返回值(回調(diào)成功的內(nèi)容)

3.如果 await 右側(cè)是一個(gè) promise 對(duì)象,則 await 返回的是 promise 成功的值。

注意:

1.await 必須寫在 async 函數(shù)中,但 async 函數(shù)中可以沒有 await。

2.如果 await 的 promise 失敗了,就會(huì)拋出異常,該異常需要通過(guò) try catch 捕獲處理。

3.如果它等到的是一個(gè) Promise 對(duì)象,await 就忙起來(lái)了,它會(huì)阻塞后面的代碼,等著 Promise 對(duì)象 resolve,然后得到 resolve 的值,作為 await 表達(dá)式的運(yùn)算結(jié)果。

正常情況下,await命令后面是一個(gè) Promise對(duì)象,返回該對(duì)象的結(jié)果。如果不是 Promise對(duì)象,就直接返回對(duì)應(yīng)的值

async function f(){    // 等同于    // return 123    return await 123}f().then(v => console.log(v)) // 123

不管await后面跟著的是什么,await都會(huì)阻塞后面的代碼

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // 阻塞
}
async function fn2 (){
    console.log('fn2')
}
fn1()
console.log(3)

上面的例子中,await 會(huì)阻塞下面的代碼(即加入微任務(wù)隊(duì)列),先執(zhí)行 async外面的同步代碼,同步代碼執(zhí)行完,再回到 async 函數(shù)中,再執(zhí)行之前阻塞的代碼

所以上述輸出結(jié)果為:1,fn2,3,2

流程分析

通過(guò)對(duì)上面的了解,我們對(duì)JavaScript對(duì)各種場(chǎng)景的執(zhí)行順序有了大致的了解

這里直接上代碼:

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')
})
async1()new Promise(function (resolve) {
    console.log('promise1')
    resolve()}).then(function () {
    console.log('promise2')
})
console.log('script end')

分析過(guò)程:

  1. 執(zhí)行整段代碼,遇到 console.log('script start') 直接打印結(jié)果,輸出 script start
  2. 遇到定時(shí)器了,它是宏任務(wù),先放著不執(zhí)行
  3. 遇到 async1(),執(zhí)行 async1 函數(shù),先打印 async1 start,下面遇到await怎么辦?先執(zhí)行 async2,打印 async2,然后阻塞下面代碼(即加入微任務(wù)列表),跳出去執(zhí)行同步代碼
  4. 跳到 new Promise 這里,直接執(zhí)行,打印 promise1,下面遇到 .then(),它是微任務(wù),放到微任務(wù)列表等待執(zhí)行
  5. 最后一行直接打印 script end,現(xiàn)在同步代碼執(zhí)行完了,開始執(zhí)行微任務(wù),即 await下面的代碼,打印 async1 end
  6. 繼續(xù)執(zhí)行下一個(gè)微任務(wù),即執(zhí)行 then 的回調(diào),打印 promise2
  7. 上一個(gè)宏任務(wù)所有事都做完了,開始下一個(gè)宏任務(wù),就是定時(shí)器,打印 settimeout

所以最后的結(jié)果是:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout

題目如下:

async function async1(){
    console.log('1')
    await async2()
    console.log('2')
}
async function async2(){
    console.log('3')
}
console.log('4')
setTimeout(function(){
    console.log('5') 
},0)  
async1();
new Promise(function(resolve){
    console.log('6')
    resolve();
}).then(function(){
    console.log('7')
})
console.log('8')

答案如下:
。。。。。。。。

原文鏈接:js原理之事件循環(huán)Event Loop 宏任務(wù)與微任務(wù) async與await

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容