第8章 JS 異步進階【想要進大廠,更多異步的問題等着你】

返回章節目錄

目錄

1.爲什麼要有Event Loop?

2.請描述event loop(事件循環/事件輪詢)的機制,可畫圖

3.Promise有哪三種狀態?如何變化?

Promise小試身手

4.async/await

5.async/await和Promise的關係

async function的函數

async+表達式

5.關於異步獨立知識點

6.宏任務與微任務

7.Event loop 和DOM渲染

8.爲什麼微任務執行時機比宏任務早?

小試牛刀1(過程梳理)

小試牛刀2(字節爛大街的筆試題過程梳理)


 

點贊再看,養成好習慣,總結不易,花了斷斷續續一個月零散時間才總結出來,老鐵多多支持~

除了視頻基本內容,本篇加上了我自己的總結,所以會更多一些內容。

 

1.爲什麼要有Event Loop?

因爲Javascript設計之初就是一門單線程語言,因此爲了實現主線程的不阻塞,Event Loop這樣的方案應運而生。

 

2.請描述event loop(事件循環/事件輪詢)的機制,可畫圖

因爲js是單線程運行的,所以異步要基於回調來實現,而event loop就是異步回調的實現原理

JS先把同步代碼執行完再去執行異步代碼,如果某一行執行報錯,則停止下面代碼的執行。

通過例子來講event loop機制

console.log("Hi");

setTimeout(function cb1() {
    console.log("cb1"); // cb即callback
}, 5000);

console.log("Bye");

運行大致過程如下(本例子缺少了微任務隊列,大家對比想象一下)

這個圖有點遺漏,因爲本代碼例子不涉及微任務隊列。請大家自行想象一個對比的微任務隊列。顯示的異步Web APIs只有宏任務,異步任務分爲宏任務和微任務。

同步代碼(棧裏面的代碼)順序執行,遇到異步代碼就記錄一下,在此過程中異步代碼如果是宏任務移動到Web APIs,直到定時的時間到就放入宏任務隊列,即圖中的Callback Queue,如果是微任務則放入微任務隊列(圖中沒畫,大家發揮一下想象力),不會經過Web APIs。如果Call Stack調用棧爲空(即同步代碼執行完),去查看微任務隊列,任務執行完就出隊列,直到微任務隊列微空後,嘗試DOM渲染(如果DOM結構發生變化),然後Event Loop開始工作,然後輪詢查找宏任務隊列Callback Queue,如有則移動到Call Stack執行... 每執行完一個宏任務,就會去檢查微任務隊列,若微任務隊列有,就執行到微任務爲空,再嘗試DOM渲染,然後去看宏任務隊列,繼續輪詢查找(永動機一樣不停地重複操作)。

注意:

1.這裏的Web APIs就是處理定時或者異步API的。

2.微任務是ES6語法規定的,宏任務是由瀏覽器規定的。

3.執行的順序是 執行棧中的代碼 => 微任務 => 宏任務(後面會展開講)

那如果代碼是如下

<button id="btn1">提交</button>
<script>
    console.log("Hi");
    $("#btn1").click(function(e) {
        console.log("button clicked");
    })
    console.log("Bye");
</script>

這個和上面的例子幾乎一樣,只不過回調函數放在Web APIs,點擊按鈕的時候回調函數就放在Callback Queue。從這裏可以看出DOM事件的觸發也是基於event loop的。

3.Promise有哪三種狀態?如何變化?

Promise三種狀態:pending、resolved、rejected

狀態變化:

1.pending-->resolved(成功了)   

2.pending-->rejected(失敗了)

狀態變化是不可逆的

狀態的表現

1.pending狀態不會觸發then和catch

2.resolved狀態的Promise會觸發後續的then回調函數

3.rejected狀態的Promise會觸發後續的catch回調函數

 

then和catch改變狀態

then正常返回resolved的Promise對象,裏面有報錯則返回rejected的Promise對象

catch正常返回resolved的Promise對象,裏面有報錯則返回rejected的Promise對象

Promise.reject(reason)返回一個狀態爲失敗的Promise對象,並將給定的失敗信息傳遞給對應的處理方法catch

Promise.resolve(value)返回一個狀態爲成功的Promise對象,並將成功信息傳遞給對應方法then

const p1 = Promise.resolve().then(()=>{
    return 100;
}) //Promise.resolve()返回resolved狀態的Promise對象,然後then執行完不報錯,還是返回一個resolved狀態的Promise
// console.log('p1', p1);

p1.then(()=>{ // 傳進來p1返回的100,但是沒有使用,打印123
    console.log("123");
})

const p2 = Promise.resolve().then(()=>{
    throw new Error("then error");
}) //Promise.resolve()返回resolved狀態的Promise對象,然後then執行完報錯了!返回一個rejected狀態的Promise

// console.log('p2', p2); // rejected觸發後續的catch回調
p2.then(()=>{
    console.log('456');
}).catch(err=>{
    console.error('err100', err);
}) // rejected狀態的Promise後續會觸發catch而不是then

運行結果

123
err100 Error: then error
    at <anonymous>

const p3 = Promise.reject("my error").catch(err=>{
    console.error(err);
})
console.log('p3', p3);
p3.then(()=>{
    console.log(100);
})

const p4 = Promise.reject('my error').catch(err => {
    throw new Error("catch err");
})

console.log('p4', p4);
p4.then(()=>{
    console.log(200);
}).catch(()=>{
    console.error("some err");
})

這題先打印p3和p4是因爲他們是同步代碼會先執行,後續都是微任務的代碼,這個放到後面再說,後面說完再返回來看這題一目瞭然。

Promise小試身手

Promise.resolve().then(()=>{
    console.log(1);
}).catch(()=>{
    console.log(2);
}).then(()=>{
    console.log(3);
})

1
3

解釋:Promise.resolve()返回一個resolved狀態的Promise後續觸發then回調,然後打印1,then執行完返回resolved狀態的Promise,然後再執行then,打印3,返回返回resolved狀態的Promise。因爲沒Error,沒有rejected狀態的Promise,所以不會觸發catch回調。

Promise.resolve().then(()=>{
    console.log(1);
    throw new Error('erro1');
}).catch(()=>{
    console.log(2);
}).then(()=>{
    console.log(3);
})

1
2
3

解釋:與上例的不同就是多了throw new Error('erro1');

Promise.resolve()返回一個resolved狀態的Promise後續觸發then回調,然後打印1,執行throw new Error('erro1');返回一個rejected狀態的Promise,觸發catch回調函數,打印2,接着返回resolved狀態的Promise,觸發後面then的回調,打印3,返回一個resolved狀態的Promise。

Promise.resolve().then(()=>{
    console.log(1);
    throw new Error('erro1');
}).catch(()=>{
    console.log(2);
}).catch(()=>{
    console.log(3);
})

1
2

解釋:這個和上題的不同就是最後一個回調是catch的,不是then的回調。

理由和上面一模一樣,不用多解釋,只不過打印2之後返回的是resolved狀態的Promise,後面沒有then,所有不打印3。catch裏面只要沒報錯Error,那麼就是resolved狀態的Promise。

我個人覺得需要額外注意的點:大家不要忽略最後的返回值,返回值會鏈式傳遞給下一個回調,只不過我們這裏的例子沒有強調返回值,等於return undefined;如果then/catch回調函數有形參,而上一個回調函數有返回值,那麼返回值會作爲下一個回調的形參。

 

4.async/await

 因爲是之前的異步回調會有callback hell(回調地獄)的問題,所有ES6出來了Promise,但是Promise的的then/catch也是基於回調函數,後來ES8出來了async/await,看起來用同步語法消滅了回調函數。

舉上一章節的一個例子

function loadImg(src) {
    const p = new Promise(
        (resolve, reject) => {
            const img = document.createElement('img')
            img.onload = () => {
                resolve(img)
            }
            img.onerror = () => {
                const err = new Error(`圖片加載失敗 ${src}`)
                reject(err)
            }
            img.src = src
        }
    )
    return p
}

const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
loadImg(src1).then(img => {
    console.log(img.width)
    return img
}).then(img => {
    console.log(img.height)
}).catch(ex => console.error(ex))

// ================上面用Promise也是不斷的處理回調=================
// 立即執行函數前面有個!,是爲了避免和上面最後一句不寫分號導致當成函數的衝突
!(async function() {
    // img1
    const img1 = await loadImg(src1);
    console.log(img1.height, img1.width);

    // img2
    const img2 = await loadImg(src2);
    console.log(img2.height, img2.width);
})();

await後面可以跟Promise對象或者async函數執行

額外提示:立即執行函數前面有個!,是爲了避免和上面最後一句不寫分號導致當成函數的衝突,比如下面的例子"abc"沒有分號,就把它當成了一個函數,因爲後面跟着"abc"(...)(),alert換行後跟着括號還是認爲是函數。你會發現平時引入js文件的時候,前面可能很多都有!就是這個原因,

 

5.async/await和Promise的關係

1.執行async函數,返回的是Promise對象

2.await相當於Promise的then

3.try...catch可捕獲異常,代替了Promise的catch

 我們來證明第一點執行async函數,返回的是Promise對象

async function fn1() {
    return 100;
}
const res1 = fn1(); // 執行async函數,返回的是一個Promise對象
console.log(res1);
res1.then(data => {
    console.log('data', data);
})

可以看到res1卻是是一個Promise對象

我們把async函數的返回改一下,直接返回一個Promise對象試試

async function fn1() {
    return Promise.resolve(100);
}
const res1 = fn1(); // 執行async函數,返回的是一個Promise對象
console.log(res1);
res1.then(data => {
    console.log('data', data);
})

可以看到打印結果data 100不變,只是Promise當時的狀態有點變化

接着看

!(async function() {
    const p1 = Promise.resolve(300);
    console.log(p1);
    const data = await p1; // 這裏await後面的語句相當於Promise的then裏面的回調函數
    console.log('data', data);
})()

這裏可以看出await後面跟着一個Promise,而執行了這一行之後,相當觸發了於Promise.then回調,拿到了300的值最後打印。

async function fn1() {
    return Promise.resolve(100);
}

!(async function() {
    const data2 = await fn1();
    console.log('data2', data2);
})()

這裏需要說明一下,這裏await後面跟着一個Promise對象,執行這一行相當於Promise.then回調,而且await這一行不執行完畢是不會去執行後面的語句。

!(async function() {
    const data1 = await 400; // 若後面不是Promise對象,則直接返回表達式執行結果,這裏是400
    console.log('data1', data1);
})()

若await後面不是Promise對象,比如字符串、函數、數字,則直接返回該表達式執行結果,這裏是400

再看一個

!(async function() {
    const p4 = Promise.reject('err1'); // rejected 狀態
    try {
        const res = await p4;
        console.log(res);
    } catch (ex) {
        console.error(ex); // try...catch相當於Promise的catch
    }
})()

這裏的try...catch相當於Promise的catch

接着看一個重要例子

!(async function() {
    const p4 = Promise.reject('err1');
    const res = await p4; // await後面的語句相當於then裏面的回調,但是這裏需要catch,這裏會報錯,後面不會執行
    console.log('res', res);
})()

可以看到這裏並沒有打印res,並沒有執行後面一句console.log,如果要解決這個問題,那麼就需要try...catch執行catch中的邏輯,就像上一個例子。改爲如下即可

 

綜上所述

如果await等待的是一個Promise對象,那麼它只想等resolved狀態的Promise,後續的語句相當與then回調纔會執行

如果等來的是rejected狀態的Promise,await接不住,必須try...catch,在catch中接住它,然後可以進行一定的自定義說明。

 

async function的函數

返回結果都是 Promise 對象(如果函數內沒返回 Promise ,則自動封裝一下)

async+表達式

await 後面跟 Promise 對象:會阻斷後續代碼,等待狀態變爲 resolved ,才獲取結果並繼續執行
await 後續跟非 Promise 對象:會直接返回

(async function () {
    const p1 = new Promise(() => {})
    await p1
    console.log('p1') // 不會執行
})()

 這裏的Promise裏面沒有resolve(), 導致await後面的表達式得不到結果,而await後面的語句都相當於Promise的then回調,只要await這裏不執行,那麼後面所有的callback都不會執行,所以不會打印"p1"

 

5.關於異步獨立知識點

function muti(num) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(num * num);
        }, 1000)
    })
}
const nums = [1, 2, 3];

nums.forEach(async (i) =>{
    const res = await muti(i);
    console.log(res);
})

根據上圖可以看到,等待1s後3個結果同時打印,那是因爲forEach循環3次已經結束了,1s的時間其實是3次循環執行到await這裏卡住了,await後面的語句相當於callback,await這裏不執行完是不會執行後面的,之後3次循環的await幾乎同時結束,瞬間打印出1,4,9

那麼如果我想要每間隔1s打印一個結果應該怎麼做呢,執行異步的循環可以用for...of

function muti(num) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(num * num);
        }, 1000)
    })
}
const nums = [1, 2, 3];

!(async function() {
    for (let i of nums) {
        const res = await muti(i);
        console.log(res);
    }
})()

 

6.宏任務與微任務

宏任務:setTimeout、setInterval、Ajax、I/OUI交互事件(比如DOM事件)

微任務:Promise回調、async/await、process.nextTick(Node獨有,註冊函數的優先級比Promise回調函數要高)MutaionObserver

微任務執行時機比宏任務要早(記住)

注意:script全部代碼、(這個是執行棧的代碼,屬於同步代碼),包括new Promise(function(){...})裏面的代碼,只有then、catch回調纔是微任務

 

console.log(100);

setTimeout(()=>{
    console.log(200);
})
// 微任務
Promise.resolve().then(()=>{
    console.log(300);
})
console.log(400);

  

 

7.Event loop 和DOM渲染

JS是單線程的,而且和DOM渲染公用一個線程,JS執行的時候,得留一些時機供DOM渲染

8.爲什麼微任務執行時機比宏任務早?

宏任務:DOM渲染後觸發,如setTimeout

微任務:DOM渲染前觸發,如Promise

爲什麼微任務在渲染前,宏任務在渲染後?

- 微任務:ES 語法標準之內,JS 引擎來統一處理。即不用瀏覽器有任何干預,可一次性處理完,更快更及時。
- 宏任務:ES 語法沒有,JS 引擎不處理,瀏覽器(或 nodejs)干預處理。

綜上所述,代碼執行順序如下:

1.call Stack清空,即同步任務執行完(執行棧內的代碼,執行完彈棧清空)

2.執行當前的微任務隊列的任務

3.嘗試DOM渲染(如果DOM結構有改變則重新渲染)

4.觸發Event Loop,執行宏任務隊列的任務

5.每執行一個宏任務會回到步驟2,檢查執行微任務,依次輪詢。

小試牛刀1(過程梳理)

setTimeout(() => {
    console.log('timeout1')
    Promise.resolve().then(() => {
        console.log('promise1')
    })
    Promise.resolve().then(() => {
        console.log('promise2')
    })
}, 100)

setTimeout(() => {
    console.log('timeout2')
    Promise.resolve().then(() => {
        console.log('promise3')
    })
}, 200)
  1. 先將兩個setTimeout塞到宏任務隊列中
  2. 當第一個setTimeout1時間到了執行的時候,首先打印timeout1,然後在微任務隊列中塞入promise1promise2
  3. 當第一個setTimeout1執行完畢後,會去微任務隊列檢查發現有兩個promise,會把兩個promise按順序執行完
  4. 嘗試DOM渲染
  5. 執行下一個宏任務,兩個promise執行完畢後會微任務隊列中沒有任務了,會去宏任務中執行下一個任務 setTimeout2
  6. setTimeout2 執行的時候,先打印一個timeout2,然後又在微任務隊列中塞了一個promise3
  7. setTimeout2執行完畢後會去微任務隊列檢查,發現有一個promise3,會將promise3執行
  8. 會依次打印 timeout1 promise1 promise2 timeout2 promise3

注意:當setTimeout定時時間間隔一樣的時候,舊版本的node可能與瀏覽器端的運行結果不一樣。 

高手想挑戰更多,請見這篇文章:Event Loop的規範和實現,這是螞蟻金服·數據體驗技術團隊掘金號的一篇文章,例子講的很細,有興趣的同學可以去看看。

小試牛刀2(字節爛大街的筆試題過程梳理)

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');
  1. 從上到下,先是2個函數定義
  2. 再打印一個script start
  3. 看到setTimeout,裏面回調函數放入宏任務隊列等待執行
  4. 接着執行async1(),打印async1 start,看到await async2(),執行後打印async2,await後面的語句相當於Promise的then回調函數,所以是微任務,console.log('async1 end')放入微任務隊列
  5. 執行new Promise,直接執行這個函數,打印promise1,執行resolve(),後續觸發的then回調是微任務,放入微任務隊列
  6. 打印script end,同步代碼執行完了
  7. 檢查微任務隊列,依次打印async1 endpromise2
  8. 嘗試DOM渲染(如果DOM結構有變化)
  9. 檢查宏任務隊列,打印setTimeout
  10. 檢查微任務隊列爲空,嘗試DOM渲染,檢查宏任務隊列爲空,執行結束

綜上,打印依次爲

爲什麼這裏有返回undefined之後纔會打印setTimeout,因爲前面是同步代碼和微任務執行完了,JS引擎工作結束,開始返回值。後面打印的setTimeout是瀏覽器處理的。

這就解釋了經常在chrome看到有了返回值再打印後續內容的問題,這個問題一般人我不告訴他,所以你趕緊偷偷存起來哈哈。

 

 

關注、留言,我們一起學習。

 

===============Talk is cheap, show me the code================

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