目錄
2.請描述event loop(事件循環/事件輪詢)的機制,可畫圖
點贊再看,養成好習慣,總結不易,花了斷斷續續一個月零散時間才總結出來,老鐵多多支持~
除了視頻基本內容,本篇加上了我自己的總結,所以會更多一些內容。
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/O
、UI交互事件(比如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)
- 先將兩個
setTimeout
塞到宏任務隊列中- 當第一個
setTimeout1
時間到了執行的時候,首先打印timeout1,然後在微任務隊列中塞入promise1
和promise2
- 當第一個
setTimeout1
執行完畢後,會去微任務隊列檢查發現有兩個promise
,會把兩個promise
按順序執行完- 嘗試DOM渲染
- 執行下一個宏任務,兩個
promise
執行完畢後會微任務隊列中沒有任務了,會去宏任務中執行下一個任務setTimeout2
- 當
setTimeout2
執行的時候,先打印一個timeout2,然後又在微任務隊列中塞了一個promise3
- 當
setTimeout2
執行完畢後會去微任務隊列檢查,發現有一個promise3,會將promise3
執行- 會依次打印
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');
- 從上到下,先是2個函數定義
- 再打印一個script start
- 看到setTimeout,裏面回調函數放入宏任務隊列等待執行
- 接着執行async1(),打印async1 start,看到await async2(),執行後打印async2,await後面的語句相當於Promise的then回調函數,所以是微任務,console.log('async1 end')放入微任務隊列
- 執行new Promise,直接執行這個函數,打印promise1,執行resolve(),後續觸發的then回調是微任務,放入微任務隊列
- 打印script end,同步代碼執行完了
- 檢查微任務隊列,依次打印async1 end和promise2
- 嘗試DOM渲染(如果DOM結構有變化)
- 檢查宏任務隊列,打印setTimeout
- 檢查微任務隊列爲空,嘗試DOM渲染,檢查宏任務隊列爲空,執行結束
綜上,打印依次爲
爲什麼這裏有返回undefined之後纔會打印setTimeout,因爲前面是同步代碼和微任務執行完了,JS引擎工作結束,開始返回值。後面打印的setTimeout是瀏覽器處理的。
這就解釋了經常在chrome看到有了返回值再打印後續內容的問題,這個問題一般人我不告訴他,所以你趕緊偷偷存起來哈哈。
關注、留言,我們一起學習。
===============Talk is cheap, show me the code================