【運(yùn)維SaaS開(kāi)發(fā)前端經(jīng)驗(yàn)分享】深入解析JS的異步機(jī)制
JavaScript定義
JavaScript 是一種單線程編程語(yǔ)言,這意味著同一時(shí)間只能完成一件事情。也就是說(shuō),JavaScript 引擎只能在單一線程中處理一次語(yǔ)句。
優(yōu)點(diǎn):?jiǎn)尉€程語(yǔ)言簡(jiǎn)化了代碼編寫,因?yàn)槟悴槐負(fù)?dān)心并發(fā)問(wèn)題,但這也意味著你無(wú)法在不阻塞主線程的情況下執(zhí)行網(wǎng)絡(luò)請(qǐng)求等長(zhǎng)時(shí)間操作。
缺點(diǎn):當(dāng)從 API 中請(qǐng)求一些數(shù)據(jù)。根據(jù)情況,服務(wù)器可能需要一些時(shí)間來(lái)處理請(qǐng)求,同時(shí)阻塞主線程,讓網(wǎng)頁(yè)無(wú)法響應(yīng)。

異步運(yùn)行機(jī)制
CallBack,setTimeOut,ajax 等都是通過(guò)事件循環(huán)(event loop)實(shí)現(xiàn)的。
1.什么是Event Loop?
主線程運(yùn)行的時(shí)候,產(chǎn)生堆(heap)和棧(stack),棧中的代碼調(diào)用各種外部API,它們?cè)凇比蝿?wù)隊(duì)列”中加入各種事件(click,load,done)。只要棧中的代碼執(zhí)行完畢,主線程就會(huì)去讀取”任務(wù)隊(duì)列”,依次執(zhí)行那些事件所對(duì)應(yīng)的回調(diào)函數(shù)。
2.流程整體示意圖

3.總結(jié)異步運(yùn)行到整體機(jī)制
主線程在運(yùn)行的時(shí)候,將產(chǎn)生堆(heap)和棧(stack),棧中的代碼會(huì)調(diào)用各種外部API,它們將在”任務(wù)隊(duì)列”中根據(jù)類型不同,分類加入到相關(guān)任務(wù)隊(duì)列中,如各種事件等。只要棧中的代碼執(zhí)行完畢,主線程就會(huì)去讀取”任務(wù)隊(duì)列”,根據(jù)任務(wù)隊(duì)列的優(yōu)先級(jí)依次執(zhí)行那些事件所對(duì)應(yīng)的回調(diào)函數(shù)。這就是整體的事件循環(huán)。
4.任務(wù)隊(duì)列的優(yōu)先級(jí)
微任務(wù)隊(duì)列中的所有任務(wù)都將在宏隊(duì)列中的任務(wù)之前執(zhí)行。也就是說(shuō),事件循環(huán)將首先在執(zhí)行宏隊(duì)列中的任何回調(diào)之前清空微任務(wù)隊(duì)列。
舉例:
console.log('Script start');
? ? ? ?setTimeout(() => {
console.log("setTimeout 1");
? ? ? ?}, 0);
? ? ? ?setTimeout(() => {
console.log("setTimeout 2");
? ? ? ?}, 0);
new Promise ((resolve, reject) => {
? ? ? ? ? ?resolve("Promise 1 resolved");
? ? ? ?})
? ? ? ?.then(res => console.log(res))
? ? ? ?.catch(err => console.log(err));
new Promise ((resolve, reject) => {
? ? ? ? ? ?resolve("Promise 2 resolved");
? ? ? ?})
? ? ? ?.then(res => console.log(res))
? ? ? ?.catch(err => console.log(err));
console.log('Script end');
運(yùn)行結(jié)果是:
Script start
Script end
Promise 1
Promise 2
setTimeout 1
setTimeout 2
通過(guò)上述例子可以看到無(wú)論宏隊(duì)列的位置在何方,只要微隊(duì)列尚未清空,一定會(huì)先清空微隊(duì)列后,在去執(zhí)行宏隊(duì)列。下面介紹微隊(duì)列任務(wù)中比較典型的幾個(gè)API,通過(guò)相關(guān)舉例,讓你更深入理解JS的異步機(jī)制。

微任務(wù)隊(duì)列
1.Promise(ES6)
Promise,就是一個(gè)對(duì)象,用來(lái)傳遞異步操作的消息。
基礎(chǔ)用法:
var promise = new Promise(function(resolve, reject) {//異步處理邏輯//處理結(jié)束后,調(diào)用resolve返回正常內(nèi)容或調(diào)用reject返回異常內(nèi)容 ? ? ? ?}) ? ? ? ?promise.then(function(result){//正常返回執(zhí)行部分,result是resolve返回內(nèi)容 ? ? ? ?}, function(err){//異常返回執(zhí)行部分,err是reject返回內(nèi)容 ? ? ? ?}) ? ? ? ?.catch(function(reason){//catch效果和寫在then的第二個(gè)參數(shù)里面一樣。另外一個(gè)作用:在執(zhí)行resolve的回調(diào)時(shí),如果拋出異常了(代碼出錯(cuò)了),那么并不過(guò)報(bào)錯(cuò)卡死JS,而是會(huì)進(jìn)入到這個(gè)catch方法中,所以一般用catch替代then的第二個(gè)參數(shù) ? ? ? ?});
缺點(diǎn):無(wú)法取消 Promise,一旦新建它就會(huì)立即執(zhí)行,無(wú)法中途取消。其次,如果不設(shè)置回調(diào)函數(shù),Promise 內(nèi)部拋出的錯(cuò)誤,不會(huì)反應(yīng)到外部。再次,當(dāng)處于 Pending 狀態(tài)時(shí),無(wú)法得知目前進(jìn)展到哪一個(gè)階段(剛剛開(kāi)始還是即將完成)。
優(yōu)點(diǎn):Promise能夠簡(jiǎn)化層層回調(diào)的寫法,Promise的精髓是“狀態(tài)”,用維護(hù)狀態(tài)、傳遞狀態(tài)的方式來(lái)使得回調(diào)函數(shù)能夠及時(shí)調(diào)用,它比傳遞callback函數(shù)要簡(jiǎn)單、靈活的多。
用法注意點(diǎn) - 順序:
new Promise((resolve, reject) => {
? ? ? ? ? ?resolve(1);
console.log(2);
? ? ? ?}).then(r => {
console.log(r);
? ? ? ?});
運(yùn)行結(jié)果是:
2
1
說(shuō)明:立即 resolved 的 Promise 是在本輪事件循環(huán)的末尾執(zhí)行,總是晚于本輪循環(huán)的同步任務(wù)。也就是resolve(1)和console.log(2)是屬于同步任務(wù),需要全部執(zhí)行完同步任務(wù)后,再去循環(huán)到resolve的then中。
用法注意點(diǎn) - 狀態(tài):
const p1 = new Promise(function (resolve, reject) { ? ? ? ? ? ?setTimeout(() => reject(new Error('fail')), 3000); ? ? ? ?});const p2 = new Promise(function (resolve, reject) { ? ? ? ? ? ?setTimeout(() => resolve(p1), 1000); ? ? ? ?});const p3 = new Promise(function (resolve, reject) { ? ? ? ? ? ?setTimeout(() => resolve(new Error('fail')), 1000); ? ? ? ?}); ? ? ? ?p2 ? ? ? ?.then(result => console.log("1:", result)) ? ? ? ?.catch(error => console.log("2:",error)); ? ? ? ?p3 ? ? ? ?.then(result => console.log("3:", result)) ? ? ? ?.catch(error => console.log("4:",error));
運(yùn)行結(jié)果是:
3: Error: fail
at setTimeout (async.htm:182)
2: Error: fail
at setTimeout (async.htm:174)
說(shuō)明:p1是一個(gè) Promise,3 秒之后變?yōu)閞ejected。p2和p3的狀態(tài)是在 1 秒之后改變,p2 resolve方法返回的是 p1, p3 resolve方法返回的是 拋出異常。但由于p2返回的是另一個(gè) Promise,導(dǎo)致p2自己的狀態(tài)無(wú)效了,由p1的狀態(tài)決定p2的狀態(tài)。所以后面的then語(yǔ)句都變成針對(duì)后者(p1)。又過(guò)了 2 秒,p1變?yōu)閞ejected,導(dǎo)致觸發(fā)catch方法指定的回調(diào)函數(shù)。而p3返回的是自身的resolve,所以觸發(fā)then中指定的回調(diào)函數(shù)。
用法注意點(diǎn) - then鏈的處理:
var p1 = p2 = new Promise(function (resolve){
? ? ? ? ? ?resolve(100);
? ? ? ?});
? ? ? ?p1.then((value) => {
return value*2;
? ? ? ?}).then((value) => {
return value*2;
? ? ? ?}).then((value) => {
console.log("p1的執(zhí)行結(jié)果:",value)
? ? ? ?})
? ? ? ?p2.then((value) => {
return value*2;
? ? ? ?})
? ? ? ?p2.then((value) => {
return value*2;
? ? ? ?})
? ? ? ?p2.then((value) => {
console.log("p2的執(zhí)行結(jié)果:",value)
運(yùn)行結(jié)果是:
p2的執(zhí)行結(jié)果:100
p1的執(zhí)行結(jié)果:400
說(shuō)明:p2寫法中的 then 調(diào)用幾乎是在同時(shí)開(kāi)始執(zhí)行的,而且傳給每個(gè) then 方法的 value 值都是 100。而p1中寫法則采用了方法鏈的方式將多個(gè) then 方法調(diào)用串連在了一起,各函數(shù)也會(huì)嚴(yán)格按照 resolve → then → then → then 的順序執(zhí)行,并且傳給每個(gè) then 方法的 value 的值都是前一個(gè)promise對(duì)象通過(guò) return 返回的值。
用法注意點(diǎn) - catch的處理:
var p1 = new Promise(function (resolve, reject){
? ? ? ? ? ?reject("test");
//throw new Error("test"); ?效果同reject("test");
//reject(new Error("test")); 效果同reject("test");
? ? ? ? ? ?resolve("ok");
? ? ? ?});
? ? ? ?p1
? ? ? ?.then(value => console.log("p1 then:", value))
? ? ? ?.catch(error => console.log("p1 error:", error));
? ? ? ?p2 = new Promise(function (resolve, reject){
? ? ? ? ? ?resolve("ok");
? ? ? ? ? ?reject("test");
? ? ? ?});
? ? ? ?p2
? ? ? ?.then(value => console.log("p2 then:", value))
? ? ? ?.catch(error => console.log("p2 error:", error));
運(yùn)行結(jié)果是:
p2 then: ok
p1 error: test
說(shuō)明:Promise 的狀態(tài)一旦改變,就永久保持該狀態(tài),不會(huì)再變了。不會(huì)即拋異常又會(huì)正常resolve。
2.async/await(ES7)
async基礎(chǔ)用法:
async 用于申明一個(gè) function 是異步的,返回的是一個(gè) Promise 對(duì)象。
async function testAsync() {
return "hello async";
? ? ? ?}
var result = testAsync();
console.log("1:", result);
? ? ? ?testAsync().then(result => console.log("2:", result));
async function mytest() {
//"hello async";
? ? ? ?}
var result1 = mytest();
console.log("3:", result1);
運(yùn)行結(jié)果是:
1: Promise {<resolved>: “hello async”}
3: Promise {<resolved>: undefined}
2: hello async
說(shuō)明:async返回的是一個(gè)Promise對(duì)象,可以用 then 來(lái)接收,如果沒(méi)有返回值的情況下,它會(huì)返回 Promise.resolve(undefined),所以在沒(méi)有 await 的情況下執(zhí)行 async 函數(shù),它會(huì)立即執(zhí)行,并不會(huì)阻塞后面的語(yǔ)句。這和普通返回 Promise 對(duì)象的函數(shù)并無(wú)二致。
await基礎(chǔ)用法:
await 只能出現(xiàn)在 async 函數(shù)中,用于等待一個(gè)異步方法執(zhí)行完成(實(shí)際等的是一個(gè)返回值,強(qiáng)調(diào) await 不僅僅用于等 Promise 對(duì)象,它可以等任意表達(dá)式的結(jié)果)。
function getMyInfo() {
return Promise.resolve("hello 2019!");
? ? ? ?}
async function testAsync() {
return "hello async";
? ? ? ?}
async function mytest() {
return Promise.reject("hello async");
? ? ? ?}
async function test() {
try {
const v1 = await getMyInfo();
console.log("getV1");
const v2 = await testAsync();
console.log("getV2");
const v3 = await mytest();
console.log(v1, v2, v3);
? ? ? ? ? ?} catch (error) {
console.log("error:", error);
? ? ? ? ? ?}
? ? ? ?}
? ? ? ?test();
運(yùn)行結(jié)果是:
getV1
getV2
error: hello async
說(shuō)明:await等到的如果是一個(gè) Promise 對(duì)象,await 就忙起來(lái)了,它會(huì)阻塞后面的代碼,等著 Promise 對(duì)象 resolve,然后得到 resolve 的值,作為 await 表達(dá)式的運(yùn)算結(jié)果。
放心,這就是 await 必須用在 async 函數(shù)中的原因。async 函數(shù)調(diào)用不會(huì)造成阻塞,它內(nèi)部所有的阻塞都被封裝在一個(gè) Promise 對(duì)象中異步執(zhí)行。
async/await的優(yōu)勢(shì):
很多情況下,執(zhí)行下一步操作是需要依賴上一步的返回結(jié)果的,如果當(dāng)嵌套層次較多的時(shí)候,(舉例3層的時(shí)候):
const getRequest = () => {
return promise1().then(result1 => {
//do something
return promise2(result1).then(result2 => {
//do something
return promise3(result1, result2)
? ? ? ? ? ? ? ?})
? ? ? ? ? ?})
? ? ? ?}
從上例可以看到嵌套內(nèi)容太多。此時(shí)如果用async寫法,可寫成如下:
const getRequest = async () => {
const result1 = await promise1();
const result2 = await promise2(result1);
return promise3(result1, result2);
? ? ? ?}
說(shuō)明:async / await 使你的代碼看起來(lái)像同步代碼,它有效的消除then鏈,讓你的代碼更加簡(jiǎn)明,清晰。

藍(lán)鯨智云
本文由騰訊藍(lán)鯨智云編輯發(fā)布,騰訊藍(lán)鯨智云(簡(jiǎn)稱藍(lán)鯨)軟件體系是一套基于PaaS的技術(shù)解決方案,致力于打造行業(yè)領(lǐng)先的一站式自動(dòng)化運(yùn)維平臺(tái)。目前已經(jīng)推出社區(qū)版、企業(yè)版,歡迎體驗(yàn)。
- 官網(wǎng):https://bk.tencent.com/
- 下載鏈接:https://bk.tencent.com/
- 社區(qū):https://bk.tencent.com/