JavaScript之同步與異步的使用及說(shuō)明
概要
異步,按照字面理解,指的是兩個(gè)或者兩個(gè)以上的對(duì)象或事件不同時(shí)存在或者發(fā)生(或者`多個(gè)相關(guān)事物的發(fā)生無(wú)需等待其一事物的完成),在JavaScript引擎中,
異步是相對(duì)于同步來(lái)說(shuō),所謂同步,大致可以理解為:
代碼執(zhí)行順序是按照其在腳本的順序來(lái)執(zhí)行(變量提升除外),腳本必須執(zhí)行完上一句代碼,才能接著執(zhí)行下一句語(yǔ)句。
在大多數(shù)情況下,這個(gè)是符合我們專(zhuān)注做一件事情的場(chǎng)景,但有時(shí)候,遇到一些很耗時(shí)間的任務(wù),如果采取同步的方法,會(huì)影響后續(xù)任務(wù)的執(zhí)行,只能等待這個(gè)耗時(shí)任務(wù)執(zhí)行完,才能接著執(zhí)行其他任務(wù),從而影響整體的效率。
為了解決這種耗時(shí)費(fèi)力的任務(wù)處理,JavaScript為我們提供了異步處理。所謂異步,可以理解為:把耗時(shí)的任務(wù)交給單獨(dú)的線程去處理,同時(shí)注冊(cè)一個(gè)回調(diào)函數(shù)。緊接著,JavaScript引擎無(wú)需等待這個(gè)任務(wù)處理完成,可以接著運(yùn)行下面的語(yǔ)句。
等到那個(gè)單獨(dú)線程處理完成任務(wù)后,告訴JavaScript引擎,JavaScript再執(zhí)行其注冊(cè)的回調(diào)函數(shù)。這個(gè)就是異步。
同步代碼
const name = "Miriam";
const greeting = `Hello, my name is ${name}!`;
console.log(greeting);
// "Hello, my name is Miriam!"
這段代碼:
- 聲明了一個(gè)叫做 name 的字符串常量
- 聲明了另一個(gè)叫做 greeting 的字符串常量(并使用了 name 常量的值)
- 將 greeting 常量輸出到 JavaScript 控制臺(tái)中。
異步代碼
var xhr = new XMLHttpRequest();
xhr.open("GET", "/bar/foo.txt", true);
xhr.onload = function (e) {
console.log("比最后一段代碼執(zhí)行更晚")
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.error(xhr.statusText);
}
}
};
xhr.onerror = function (e) {
console.error(xhr.statusText);
};
xhr.send(null);
console.log("執(zhí)行語(yǔ)句")
第 2 行中指定第三個(gè)參數(shù)為 true,表示該請(qǐng)求應(yīng)該以異步模式執(zhí)行。
第 3 行創(chuàng)建一個(gè)事件處理函數(shù)對(duì)象,并將其分配給請(qǐng)求的 onload 屬性。此處理程序查看請(qǐng)求的 readyState,以查看事務(wù)是否在第 4 行完成,如果是,并且 HTTP 狀態(tài)為 200,則轉(zhuǎn)儲(chǔ)接收到的內(nèi)容。如果發(fā)生錯(cuò)誤,則顯示錯(cuò)誤消息。
第 15 行實(shí)際上啟動(dòng)了請(qǐng)求。只要請(qǐng)求的狀態(tài)發(fā)生變化,就會(huì)調(diào)用回調(diào)程序。
發(fā)起請(qǐng)求后,立馬執(zhí)行console.log("執(zhí)行語(yǔ)句"),等待請(qǐng)求狀態(tài)發(fā)送變化,才會(huì)觸發(fā)onload注冊(cè)的函數(shù),接著執(zhí)行console.log("比最后一段代碼執(zhí)行更晚")
JavaScript運(yùn)行機(jī)制
通過(guò)上面的代碼演示,我們可以發(fā)現(xiàn),JavaScript有自己的一套運(yùn)行機(jī)制,這套機(jī)制稱(chēng)之為事件循環(huán),像異步處理這些屬于事件循環(huán)的并發(fā)模型機(jī)制。
事件循環(huán)負(fù)責(zé)執(zhí)行代碼,收集和處理事件以及執(zhí)行任務(wù)隊(duì)列中的子任務(wù)。
任務(wù)隊(duì)列子任務(wù),可以理解為異步事件注冊(cè)的回調(diào)函數(shù)。
運(yùn)行時(shí)概念
接下來(lái)的內(nèi)容解釋了這個(gè)理論模型?,F(xiàn)代 JavaScript 引擎實(shí)現(xiàn)并著重優(yōu)化了以下描述的這些語(yǔ)義。

棧(stack)
函數(shù)調(diào)用形成了一個(gè)由若干幀組成的棧。
function foo(b) {
let a = 10;
return a + b + 11;
}
function bar(x) {
let y = 3;
return foo(x * y);
}
console.log(bar(7)); // 返回 42
- 當(dāng)調(diào)用bar 函數(shù),第一個(gè)幀被創(chuàng)建并壓入棧中,幀中包含了bar 的參數(shù)和局部變量。
- 當(dāng)bar 調(diào)用foo 時(shí),第二個(gè)幀被創(chuàng)建并被壓入棧中,放在第一個(gè)幀上面,幀中包含了foo 的參數(shù)和局部變量
- 當(dāng)foo執(zhí)行完畢后返回時(shí),第二個(gè)幀就被彈出棧(剩下bar函數(shù)的調(diào)用幀)
- 當(dāng)bar 執(zhí)行完畢后,第一個(gè)幀也被彈出,棧就被清空了。
堆
對(duì)象被分配在堆中,堆是一個(gè)用來(lái)表示一大塊(通常是非結(jié)構(gòu)化的)內(nèi)存區(qū)域的計(jì)算機(jī)術(shù)語(yǔ)。
隊(duì)列
一個(gè)JavaScript運(yùn)行時(shí),包含一個(gè)待處理的消息隊(duì)列,每一個(gè)消息都關(guān)聯(lián)著一個(gè)用來(lái)處理這個(gè)消息的回調(diào)函數(shù)。
隊(duì)列和棧的元素被處理順序是相反的,隊(duì)列是先進(jìn)先出,和我們生活中先來(lái)后到的排隊(duì)機(jī)制一樣。因此,先被添加的消息,先執(zhí)行,消息要被全部執(zhí)行完畢,才能執(zhí)行下一個(gè)消息。
在事件循環(huán)期間的某個(gè)時(shí)刻,運(yùn)行時(shí)會(huì)從最先進(jìn)入隊(duì)列的消息開(kāi)始處理隊(duì)列中的消息。被處理完成的消息會(huì)被移除隊(duì)列,并作為輸入?yún)?shù)來(lái)調(diào)用與之關(guān)聯(lián)的函數(shù)(也就是回調(diào)函數(shù)),正如前面所提到的,調(diào)用一個(gè)函數(shù)總是會(huì)為其創(chuàng)造一個(gè)新的棧幀。
函數(shù)處理一直進(jìn)行到執(zhí)行棧為空為止,然后事件循環(huán)將會(huì)處理下一個(gè)消息。
消息的添加
我們?cè)谏厦媪私獾?,?duì)列用來(lái)存儲(chǔ)消息,先被添加的消息先處理,那么,消息是何時(shí)被添加到消息隊(duì)列的呢?
JavaScript 中,每當(dāng)一個(gè)指定的事件真實(shí)發(fā)生了,并且綁定了相應(yīng)回調(diào)函數(shù),一個(gè)消息就會(huì)被添加到消息隊(duì)列。如果這個(gè)事件沒(méi)有回調(diào)函數(shù),這個(gè)事件將會(huì)丟失。
就像我們給某個(gè)元素綁定了點(diǎn)擊事件,實(shí)際上只是給某個(gè)元素注冊(cè)了一個(gè)點(diǎn)擊事件,這個(gè)事件注冊(cè)的回調(diào)代碼,不會(huì)立馬執(zhí)行。而是用戶真實(shí)點(diǎn)擊這個(gè)元素時(shí),這個(gè)點(diǎn)擊事件才會(huì)真實(shí)發(fā)生,然后把消息添加到消息隊(duì)列。如果此時(shí)消息隊(duì)列,前面還有消息,那么會(huì)將點(diǎn)擊事件的消息排在后面,等待前面消息處理完成才處理這個(gè)消息,并調(diào)用相應(yīng)的回調(diào)函數(shù)。
例如以下代碼:
函數(shù) setTimeout 接受兩個(gè)參數(shù):待加入隊(duì)列的消息和一個(gè)時(shí)間值(可選,默認(rèn)為 0)。這個(gè)時(shí)間值代表了消息被實(shí)際加入到隊(duì)列的最小延遲時(shí)間。如果隊(duì)列中沒(méi)有其他消息并且棧為空,在這段延遲時(shí)間過(guò)去之后,消息會(huì)被馬上處理。但是,如果有其他消息,setTimeout 消息必須等待其他消息處理完。因此第二個(gè)參數(shù)僅僅表示最少延遲時(shí)間,而非確切的等待時(shí)間。
(function () {
console.log("這是開(kāi)始");
setTimeout(function cb() {
console.log("這是來(lái)自第一個(gè)回調(diào)的消息");
});
console.log("這是一條消息");
setTimeout(function cb1() {
console.log("這是來(lái)自第二個(gè)回調(diào)的消息");
}, 0);
console.log("這是結(jié)束");
})();
// "這是開(kāi)始"
// "這是一條消息"
// "這是結(jié)束"
// "這是來(lái)自第一個(gè)回調(diào)的消息"
// "這是來(lái)自第二個(gè)回調(diào)的消息"
因?yàn)閏b(),也是延遲0秒,因此先被添加到消息隊(duì)列。cb1排在其后面,因此晚執(zhí)行。
基本上,setTimeout 需要等待當(dāng)前隊(duì)列中所有的消息都處理完畢之后才能執(zhí)行,即使已經(jīng)超出了由第二參數(shù)所指定的時(shí)間。
異步場(chǎng)景
網(wǎng)絡(luò)請(qǐng)求
JavaScript處理后臺(tái)接口請(qǐng)求,一般都是使用XMLHttpRequest 進(jìn)行異步請(qǐng)求。如流行的axios庫(kù),也是基于XMLHttpRequest。
下面代碼演示axios 請(qǐng)求接口
//為給定 ID 的 user 創(chuàng)建請(qǐng)求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
console.log("123")
上面代碼,使用axios 的get方法發(fā)起一個(gè)get請(qǐng)求,在then 注冊(cè)接口成功返回的函數(shù)。
最后console.log(“123”)的語(yǔ)句一般先于console.log(response);執(zhí)行。
- JavaScript將網(wǎng)絡(luò)請(qǐng)求交給
網(wǎng)絡(luò)請(qǐng)求線程處理,JavaScript引擎此時(shí)不需要等待其執(zhí)行完畢,就可以接著執(zhí)行console.log(“123”) - 如果請(qǐng)求接口返回了數(shù)據(jù),這個(gè)時(shí)候,JavaScript會(huì)將消息添加到消息隊(duì)列,如果此時(shí)隊(duì)列沒(méi)有其他消息,便可以立即處理這個(gè)消息,然后調(diào)用其關(guān)聯(lián)的回調(diào)函數(shù):function (response) {console.log(response);}
網(wǎng)絡(luò)請(qǐng)求線程與JavaScript引擎工作如下圖所示:

以 Chrome 為例,瀏覽器不僅有多個(gè)線程,還有多個(gè)進(jìn)程,如渲染進(jìn)程、GPU 進(jìn)程和插件進(jìn)程等。
每個(gè) tab 標(biāo)簽頁(yè)都是一個(gè)獨(dú)立的渲染進(jìn)程,所以一個(gè) tab 異常崩潰后,其他 tab 基本不會(huì)被影響。
渲染進(jìn)程下包含了 JS 引擎線程、HTTP 請(qǐng)求線程和定時(shí)器線程等,這些線程為 JS 在瀏覽器中完成異步任務(wù)提供了基礎(chǔ)。
異步編程優(yōu)化
在我們?nèi)粘>幊讨?,需要用到很多關(guān)于異步編程的地方,比如網(wǎng)絡(luò)請(qǐng)求,很多時(shí)候,碰到一些復(fù)雜的需求,比如請(qǐng)求A ,返回結(jié)果后,需要作為請(qǐng)求B的參數(shù),請(qǐng)求B返回結(jié)果,需要作為 C的請(qǐng)求參數(shù),如果用普通的異步編程,可以得到以下的代碼:
axios.get('A')
.then(function (reA) {
axios.get('B',reA)
.then(function (reB) {
axios.get('C',reB)
.then(function (reC) {
console.log("請(qǐng)求完成")
})
.catch(function (errorC) {
console.log(errorC);
});
})
.catch(function (errorB) {
console.log(errorB);
});
})
.catch(function (errorA) {
console.log(errorA);
});
以上代碼我們稱(chēng)為回調(diào)地獄,就是多層回調(diào)嵌套,造成可讀性和可維護(hù)性難度加大。
為了解決上訴的問(wèn)題,ES6提供了 promise 。
我們可以使用promise 來(lái)優(yōu)化上面的回調(diào)地獄程序
async function geAllData() {
const resA = await getData("A")
const resB = await getData("B",resA)
const resC = await getData("C",resB)
console.log("請(qǐng)求完成",resC)
}
function getData(url, params) {
return new Promise((resolve, reject) => {
axios.get(url,params)
.then(function (res) {
resolve(res)
})
.catch(function (error) {
reject(error);
});
})
}
geAllData()
console.log("geAllData調(diào)用后立馬執(zhí)行");
上面的代碼中,getData 函數(shù)里面,我們返回一個(gè)Promise 的實(shí)例對(duì)象,其中傳入一個(gè)回調(diào)函數(shù),參數(shù)為resolve, reject,當(dāng)我們接口返回成功時(shí)調(diào)用resolve,失敗則調(diào)用reject。
geAllData 函數(shù)使用了 async 關(guān)鍵字,告訴JavaScript,這個(gè)是一個(gè)異步函數(shù),這個(gè)函數(shù)里面可以使用await關(guān)鍵字。
await關(guān)鍵字可以從字面上理解,就是等待,等待接口getData 函數(shù)執(zhí)行完resolve語(yǔ)句,代碼才會(huì)繼續(xù)往下執(zhí)行,如果一直沒(méi)有調(diào)用resolve或者reject,那么,就會(huì)一直等待,不會(huì)執(zhí)行下面的語(yǔ)句。
通過(guò)await 獲取的值,就是resolve 函數(shù)的參數(shù)值,比如const resA 就是 resolve(res)中res 的值。
注意事項(xiàng):
同步只有發(fā)生在async 修飾的函數(shù)內(nèi)部,在外部還是不影響其他代碼執(zhí)行的。比如console.log(“geAllData調(diào)用后立馬執(zhí)行”) 語(yǔ)句不會(huì)等待geAllData 執(zhí)行完console.log("請(qǐng)求完成",resC) 才執(zhí)行,而是調(diào)用geAllData函數(shù)后,立馬執(zhí)行的。
通過(guò)上面的優(yōu)化,我們可以將異步的代碼,以同步代碼的寫(xiě)法來(lái)實(shí)現(xiàn),這樣提高了代碼可讀性,后續(xù)文章會(huì)針對(duì)Promise 做一個(gè)詳細(xì)介紹。
總結(jié)
- JavaScript引擎是單線程的,它通過(guò)
事件循環(huán)機(jī)制來(lái)不斷處理消息隊(duì)列的消息 - 消息隊(duì)列的消息只有對(duì)應(yīng)的事件
真實(shí)發(fā)生,并綁定了相應(yīng)的回調(diào)函數(shù),才會(huì)被添加到消息隊(duì)列 - 消息隊(duì)列先被添加的消息先被處理(
先進(jìn)先出) - JavaScript引擎處理異步任務(wù),是交給另外線程去處理,等另一個(gè)線程處理完成,才把消息添加到JavaScript運(yùn)行時(shí)的消息隊(duì)列。
- setTimeout 延時(shí)0 秒,
不代表0秒后立馬執(zhí)行,要看延時(shí)到指定時(shí)間后,消息隊(duì)列有無(wú)排在其前面的消息,如果有,則要先處理前面的消息,才執(zhí)行延時(shí)任務(wù)。 - 回調(diào)地獄可以使用
Promise+async+await優(yōu)化 - async修飾的函數(shù)內(nèi)部,用await處理是同步執(zhí)行的,而async函數(shù)外部,不受影響。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
淺析JavaScript中的特殊數(shù)據(jù)類(lèi)型
在JavaScript中,有6大數(shù)據(jù)類(lèi)型,分別包括string,number,boolean,undefined,null 和 object。下面通過(guò)通過(guò)本文給大家介紹JavaScript中的特殊數(shù)據(jù)類(lèi)型,需要的朋友參考下吧2017-12-12
JS圖片放大效果簡(jiǎn)單實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了JS實(shí)現(xiàn)圖片放大效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09
Javascript基礎(chǔ)教程之比較null和undefined值
這篇文章主要介紹了Javascript基礎(chǔ)教程之比較null和undefined值的相關(guān)知識(shí),非常不錯(cuò),具有參考借鑒價(jià)值,感興趣的朋友一起學(xué)習(xí)吧2016-05-05
修改js confirm alert 提示框文字的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇修改js confirm alert 提示框文字的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-06-06
JavaScript實(shí)現(xiàn)多種彈窗效果實(shí)戰(zhàn)詳解(實(shí)現(xiàn)方法)
本文詳細(xì)講解如何使用JS結(jié)合HTML和CSS實(shí)現(xiàn)對(duì)聯(lián)彈窗、可移動(dòng)彈窗和右下角消息彈窗,并涵蓋事件監(jiān)聽(tīng)、元素定位、顯示控制等關(guān)鍵技術(shù),本文給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2025-10-10
JavaScript實(shí)現(xiàn)定時(shí)器循環(huán)展示數(shù)組
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)定時(shí)器循環(huán)展示數(shù)組,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-09-09
一文讀懂JS中的var/let/const和暫時(shí)性死區(qū)
這篇文章主要為大家詳細(xì)介紹了JavaScript中的var、let、const和暫時(shí)性死區(qū)的異同,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-02-02
JS實(shí)現(xiàn)數(shù)組扁平化的方法總結(jié)
數(shù)組扁平化相信不少朋友在一些面試中被問(wèn)到過(guò),這在我們?nèi)粘>幊讨幸彩且粋€(gè)常規(guī)操作,它需要我們將一個(gè)多維數(shù)組轉(zhuǎn)化成一個(gè)一維數(shù)組,所以,借著這篇文章,我們今天就一起來(lái)匯總一下幾種數(shù)組扁平化的方式,需要的朋友可以參考下2024-02-02

