日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

写给初学者的JavaScript异步编程和背后思想

發(fā)布時間:2025/4/16 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 写给初学者的JavaScript异步编程和背后思想 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

導(dǎo)讀: 對于接觸JavaScript這門編程語言沒有多久的本菜雞而言,在相當(dāng)長的一段時間內(nèi),我都完全無法理解這門語言中的異步編程,不明白什么叫異步編程以及為什么需要異步編程。為什么順序執(zhí)行程序就不行了呢?非要使用異步回調(diào)的方式來去做?經(jīng)過一段時間的學(xué)習(xí)和探究,我算是初步了解了其中的道理和內(nèi)涵。如果你像我一樣也是一個JavaScript的小白,希望你看完我寫的這篇文章之后,也可以解答你內(nèi)心的很多困惑,并對JavaScript這門編程語言可以有一個更為深入的了解。


從單線程語言講起

很長一段時間,我對JavaScript語言的困惑來自于我不清楚什么是單線程編程語言,而這個特性對于JavaScript走向異步編程的方式至關(guān)重要。在我們開始去講單線程編程語言之前,有必須先去了解什么是進(jìn)程,什么是線程。對這兩個概念如果用純文字講解略顯蒼白,這里推薦一個B站Up主對線程和進(jìn)程講解的視頻。我就是看完這個視頻之后才明白進(jìn)程和線程之間的關(guān)系的,相信你看完之后也可以理解。

有了之前的概念基礎(chǔ),下面就來談?wù)勈裁词菃尉€程編程語言。單線程顧名思義就是一個進(jìn)程里面只會有一個線程。這一個線程有的時候也會被稱之為主線程。如果一個進(jìn)程里面只能有一個線程的話,那它就只能串行的執(zhí)行程序,就是只能執(zhí)行完A任務(wù),然后再去執(zhí)行B任務(wù)。反之,如果一個進(jìn)程里面有多個線程的話,那么它看起來好像是可以并行的執(zhí)行多個任務(wù)。對于單核CPU來說,系統(tǒng)會在多個線程之間快速不停地切換,就可以給你一種錯覺好像是多個線程在并行的執(zhí)行,但這只是一種錯覺而已。真正的并行應(yīng)該是多核才能實(shí)現(xiàn)。關(guān)于串行、并行和并發(fā)這幾個概念,引用知乎某大佬一個形象的回答:

你吃飯吃到一半,電話來了,你一直到吃完了以后才去接,這就說明你不支持并發(fā)也不支持并行。
你吃飯吃到一半,電話來了,你停了下來接了電話,接完后繼續(xù)吃飯,這說明你支持并發(fā)。
你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支持并行。

并發(fā)的關(guān)鍵是你有處理多個任務(wù)的能力,不一定要同時。并行的關(guān)鍵是你有同時處理多個任務(wù)的能力。所以我認(rèn)為它們最關(guān)鍵的點(diǎn)就是:是否是『同時』。

多線程就類似于并發(fā)這樣的一個方式。只有多核才能做到真正的并行。

那么為什么JavaScript會采用這種單線程編程語言的方式呢?可以設(shè)想一下,如果JavaScript是一門支持多線程編程語言(比如Java),而多線程實(shí)際上是CPU在多個線程之間來回快速切換而已,當(dāng)JavaScript運(yùn)行在瀏覽器中,這時一個線程說要刪除一個圖片,另外一個線程說要添加一個圖片,如果有上百個線程都在爭搶CPU的運(yùn)行時間的話,你的瀏覽器頁面就會有很大的不確定性了,我們并不希望自己的頁面加載、更新有很大不確定性。而如果一個進(jìn)程只有一個線程的話,就不會有上述問題,任務(wù)一個接一個順序執(zhí)行就可以了。


單線程語言的問題

單線程的確可以不用考慮煩人的哪個任務(wù)先執(zhí)行,哪個任務(wù)后執(zhí)行的問題了,也不需要考慮多線程之間不同線程數(shù)據(jù)同步的問題。但是線程有他自身的問題。因?yàn)閱尉€程是按順序執(zhí)行,如果一個任務(wù)執(zhí)行的很慢的話,后面的任務(wù)全部都會被阻塞,執(zhí)行不了。

想象你去超市買東西,如果前面有一個人結(jié)賬很慢,那你也只能干等著,別的什么事也做不了。對于任務(wù)也是同樣的道理,對于瀏覽器而言。我們通常需要各種各樣的網(wǎng)絡(luò)請求,去獲取圖片,視頻等網(wǎng)絡(luò)資源。如果一個網(wǎng)絡(luò)請求很慢,比如某一個圖片加載很慢,那后面的所有的數(shù)據(jù)都只能等著前面圖片加載完之后才能再去訪問網(wǎng)絡(luò)加載。但我們實(shí)際用瀏覽器訪問網(wǎng)頁體驗(yàn)好像并不是這樣。比如有的時候可能某一個圖片加載很長時間也沒有加載出來,但這并沒有影響后面的圖片或者視頻等資源的加載。我們是如何做到這一點(diǎn)的呢?答案是使用異步編程(終于講到異步編程了^ – ^)


想不清楚是因?yàn)橥丝紤]宿主環(huán)境!

我們首先引用一下lynnelv關(guān)于同步和異步的定義。


老實(shí)說,看完之后我其實(shí)是挺疑惑的!JavaScript是一個單線程語言,你比如說你有一個調(diào)用棧,就像在超市排隊(duì)結(jié)賬一樣,按順序執(zhí)行程序,這很好理解。但怎么又出現(xiàn)了一個任務(wù)隊(duì)列!單線程還能一邊搞調(diào)用棧還能一邊去做任務(wù)隊(duì)列嗎?!,舉一個例子:

function updateAsync() {var i = 0;function updateLater() {document.getElementById('output').innerHTML = (i++);if (i < 1000) {setTimeout(updateLater, 0);}}updateLater(); }

比如上面代碼展示的setTimeout是一個異步執(zhí)行程序(書上就是這么說的),但是!單線程怎么可能會做到異步呢?假設(shè)你去排隊(duì)結(jié)賬,而且因?yàn)閱尉€程,所以只能有一個結(jié)賬通道,那么前面人結(jié)賬再久,你也只能等著。你不能換一個通道去結(jié)賬。但這個任務(wù)隊(duì)列就是告訴你,我們還有一個結(jié)賬通道。比如這個setTimeout是異步函數(shù),他的延時計(jì)時,不會影響你的主線程執(zhí)行。可是如果你只有一個線程,要么去執(zhí)行程序,要么去計(jì)時,怎么可能一邊計(jì)時,一邊還執(zhí)行程序呢?

后來我才明白,原來是這么回事!除了JavaScript本身之外,還有一個宿主環(huán)境。

JavaScript本身是不能自己單獨(dú)執(zhí)行的,要么在瀏覽器中,要么是在Node.js中,而上述兩個就是宿主環(huán)境。JavaScript本身是單線程編程語言這沒錯,但JavaScript的宿主環(huán)境給他提供了額外的并發(fā)功能。(也就是上面說的消息隊(duì)列)

上圖中,主線程運(yùn)行的時候,產(chǎn)生堆(heap)和棧(stack),棧中的代碼調(diào)用各種外部API,它們在"任務(wù)隊(duì)列"中加入各種事件(click,load,done)。只要棧中的代碼執(zhí)行完畢,主線程就會去讀取"任務(wù)隊(duì)列",依次執(zhí)行那些事件所對應(yīng)的回調(diào)函數(shù)。

上圖和對應(yīng)的文字內(nèi)容引用自JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop,圖中的WebAPIs就是JavaScript宿主環(huán)境所提供的線程。setTimeout會在該線程中執(zhí)行,并不會影響JavaScript自身的主線程。setTimeout的延時時間結(jié)束之后,它就會進(jìn)入到圖中的回調(diào)消息隊(duì)列中,當(dāng)主線程空閑時,消息隊(duì)列中的內(nèi)容就會進(jìn)入到主線程中去執(zhí)行。

有了異步編程這樣一個東西之后,JavaScript即使是單線程編程,也可以有并發(fā)的特性了。將網(wǎng)絡(luò)請求和I/O操作等耗時的任務(wù)都交給異步編程來實(shí)現(xiàn),主線程把控整體流程,這樣主線程就不會被耗時任務(wù)拖累而發(fā)生阻塞。


Ajax

我們之前已經(jīng)花了很大的篇幅來介紹異步編程和背后的思想。在這一部分我們談?wù)剬τ跒g覽器而言,最重要的異步編程應(yīng)用AJAX。

Ajax 即“Asynchronous Javascript And XML”(異步 JavaScript 和 XML),是指一種創(chuàng)建交互式、快速動態(tài)網(wǎng)頁應(yīng)用的網(wǎng)頁開發(fā)技術(shù),無需重新加載整個網(wǎng)頁的情況下,能夠更新部分網(wǎng)頁的技術(shù)。

通過在后臺與服務(wù)器進(jìn)行少量數(shù)據(jù)交換,Ajax 可以使網(wǎng)頁實(shí)現(xiàn)異步更新。這意味著可以在不重新加載整個網(wǎng)頁的情況下,對網(wǎng)頁的某部分進(jìn)行更新。

因?yàn)楫惒讲僮魍枰玫絚allback,如果不太清楚callback的話,可以看一下這篇文章,里面的關(guān)于callback的比喻很形象。因?yàn)楫惒讲僮鞑皇峭饺蝿?wù),把異步請求或者操作發(fā)送出去之后就繼續(xù)執(zhí)行同步任務(wù)了,所以很適合用這種執(zhí)行完之后“打電話”回調(diào)的方式執(zhí)行任務(wù)。

知道callback的原理之后我們繼續(xù)回到AJAX。下面這段代碼是模擬AJAX請求的代碼:

function ajax(url, callback) {// 1、創(chuàng)建XMLHttpRequest對象var xmlhttpif (window.XMLHttpRequest) {xmlhttp = new XMLHttpRequest()} else { // 兼容早期瀏覽器xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')}// 2、發(fā)送請求xmlhttp.open('GET', url, true)xmlhttp.send()// 3、服務(wù)端響應(yīng)xmlhttp.onreadystatechange = function () {if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {var obj = JSON.parse(xmlhttp.responseText)// console.log(obj)callback(obj)}} }

當(dāng)服務(wù)器有響應(yīng)之后,把響應(yīng)數(shù)據(jù),通過回調(diào)函數(shù)返回回來。注意之前文章中說的回調(diào)函數(shù)特點(diǎn),是程序執(zhí)行完把響應(yīng)回調(diào)回來。在服務(wù)器響應(yīng)這種異步的情況下,是很適合使用回調(diào)函數(shù)的。我們執(zhí)行上述ajax的代碼可以如下:

var url = 'https://getman.cn/mock/route/to/demo' ajax(url, res => {console.log(res) })

這個URL路徑是一個在線測試網(wǎng)站,你可以點(diǎn)擊這里查看該網(wǎng)站詳情。如上所示,通常情況下我們定義回調(diào)函數(shù)的時候,是寫成箭頭函數(shù)的形式,在該函數(shù)里面具體定義要執(zhí)行什么樣的回調(diào)動作。為了便于理解,我把箭頭函數(shù)改成普通函數(shù)的形式,代碼如下:

ajax(url, function(res) {console.log(res) })

最后再強(qiáng)調(diào)一下,我們是把一個函數(shù)作為參數(shù)傳遞進(jìn)去了。這個函數(shù)就是回調(diào)函數(shù)。

理解了AJAX和回調(diào)函數(shù)之后,我們再來看下面一種情況。在實(shí)際編程中,我們經(jīng)常會遇到執(zhí)行某些請求,然后有了反饋再出發(fā)a動作,a動作執(zhí)行完,有了反饋再觸發(fā)b動作…。舉一個例子,比如客戶端請求一段視頻內(nèi)容,這時候服務(wù)器不會將整段視頻全部傳過去,而是切分成很多小段,一段一段傳給客戶端。客戶端得到一段視頻之后確認(rèn)無誤,會再次發(fā)送請求,服務(wù)端就會再傳過來下一段內(nèi)容。而這些操作都是要異步完成,如果我們寫成回調(diào)函數(shù),大概可能會長這樣:

ajax(url, res => {dosomethingajax(url, res => {dosomethingajax(url, res => {dosomething......})}) })

很可能會嵌套非常多的層數(shù),從可讀性來講,非常的差,這種代碼結(jié)構(gòu)稱之為回調(diào)地獄。對于這種回調(diào)地獄的寫法問題,我們可以通過promise來解決。


Promise

ES6中新增一個引用類型稱之為Promise,可以通過new操作符來實(shí)例化

我們來舉一個簡單例子:

let p = new Promise(() => {setTimeout(() => {console.log('wait one second')}, 1000) })

但需要特別注意的,特別是對于初學(xué)者而言,我上面雖然寫的setTimeout是異步執(zhí)行,如果去執(zhí)行代碼的話,也的確會停留1s之后再去輸出。但這并不表示new Promise里面的所有代碼都會異步執(zhí)行。我們下面舉一個例子:

let p = new Promise(() => {console.log(1) }) console.log(2)

如果你打印輸出的話,會發(fā)現(xiàn),先輸出的1,后輸出的2。顯然,執(zhí)行console.log(2)是同步任務(wù),如果Promise是異步的話,應(yīng)該先打印2后打印1。因此,需要特別注意的是,Promise里面的代碼是同步任務(wù),是立刻執(zhí)行的。可能這時候你有點(diǎn)糊涂了,我了解同步代碼同步執(zhí)行,異步代碼異步執(zhí)行(比如setTimeout)。那Promise有什么用呢?又怎么實(shí)現(xiàn)異步操作?別急,關(guān)于這一點(diǎn),我們后面就會談到。

對于Promise而言,它最重要的特點(diǎn)在于其具有狀態(tài)管理的功能。它一共有三種狀態(tài):

  • pending
  • resolved
  • rejected

Promise的一個特點(diǎn)就是它只能從pending狀態(tài)到resolved狀態(tài),或者從pending狀態(tài)到rejected狀態(tài)。不能反過來。或者它也可能一直處于pending狀態(tài),當(dāng)我們new一個Promise的時候,它就是pending狀態(tài)。

那這個狀態(tài)有什么用?舉一個簡單例子:

let p = new Promise((resolve, reject) => {console.log('進(jìn)入Promise')resolve('成功')// 因?yàn)閜romise狀態(tài)是不可逆的,所以reject實(shí)際是執(zhí)行不了的reject('失敗') }).then(res => {console.log(res) }, err => {console.log(err) })

你可以執(zhí)行一下上述代碼,看看會輸出什么。

我們在new Promise的時候傳入兩個參數(shù),分別是resolve和reject(需要注意的是這兩個參數(shù)都是函數(shù))。調(diào)用resolve()會把狀態(tài)切換成resolved,調(diào)用reject()會把狀態(tài)切換成rejected(同時會拋出一個錯誤)。就像我們前面說的,狀態(tài)是不可逆的。所以上述代碼執(zhí)行完resolve()之后,reject()是不會被執(zhí)行的。

在上面的代碼中我們還看到.then方法,Promise.prototype.then()是為Promise實(shí)例添加處理程序的主要方法。執(zhí)行上述代碼我們也可以看到,resolve()或者reject()所傳遞的參數(shù)會進(jìn)入到then中進(jìn)行對應(yīng)的處理。

通過上面的例子,我們就可以大致勾勒出Promise的應(yīng)用場景。比如客戶端通過Promise發(fā)送一個異步請求給服務(wù)器。服務(wù)器如果響應(yīng)成功,就在resolve所對應(yīng)的then中去執(zhí)行對應(yīng)代碼,如果響應(yīng)失敗,就進(jìn)入到reject,在reject對應(yīng)的then中去執(zhí)行對應(yīng)代碼。

另外,關(guān)于請求失敗的代碼,除了用then的第二個參數(shù)捕獲之外,還可以寫成catch的形式:

let p = new Promise((resolve, reject) => {console.log('進(jìn)入Promise')resolve('成功')reject('失敗') }).then(res => {console.log(res) }).catch(err => {console.log(err) })

以上,關(guān)于Promise最為基礎(chǔ)的部分介紹的已經(jīng)足夠多了,下面我們嘗試用Promise來調(diào)用一下AJAX請求。

let p = new Promise((resolve, reject) => {console.log('客戶端向服務(wù)端發(fā)送請求')ajax(url, res => {console.log('第一次獲得的響應(yīng)結(jié)果是', res)resolve('成功')}) }).then(resolveRes => {console.log('請求結(jié)果:', resolveRes)return new Promise((resolve, reject) => {ajax(url, res => {console.log('第二次獲得的響應(yīng)結(jié)果是', res)resolve('成功')}) }) }).then(resolveRes => {console.log('請求結(jié)果:', resolveRes)return new Promise((resolve, reject) => {ajax(url, res => {console.log('第三次獲得的響應(yīng)結(jié)果是', res)resolve('成功')}) }) })

上述代碼模擬的是比如客戶端多次向服務(wù)器發(fā)送視頻請求的功能代碼。看起來一定程度上減輕了回調(diào)地獄這種糟糕寫法,將嵌套結(jié)構(gòu)變成了鏈?zhǔn)浇Y(jié)構(gòu)。但看起來好像非常糟糕。一方面是大量重復(fù)代碼,非常冗余,另一方面,從理解層面也沒有改善多少。關(guān)于第二點(diǎn),后面我們會介紹一種新的語法結(jié)構(gòu):async,await。不過現(xiàn)在我們還是繼續(xù)回到Promise中。

上面的代碼,需要特別強(qiáng)調(diào)的一點(diǎn)是,每次我們new一個Promise,都會加一個return,為什么要寫return呢?

比如在第二次請求的那個對應(yīng)的Promise中不寫return的話,意味著就不會有返回值。后面的then執(zhí)行,相當(dāng)于對戰(zhàn)原有的(就是最開始的那個)Promise對象的then繼續(xù)再執(zhí)行then(返回的會是一個空的promise)。只有return 之后,才意味著對新new出來的Promise再執(zhí)行then。因此在書寫代碼的時候一定要記得加return,否則如果發(fā)生多層Promise嵌套的時候很有可能出現(xiàn)邏輯錯誤。

下面我們把上面代碼中重復(fù)部分進(jìn)行抽離,精簡一下代碼可以寫成這樣:

function getPromise(url) {return new Promise((resolve, reject) => {ajax(url, res => {console.log('獲得的響應(yīng)結(jié)果是', res)})resolve('成功')}) }getPromise(url).then(resolveRes => {console.log('請求結(jié)果:', resolveRes)return getPromise(url) }).then(resolveRes => {console.log('請求結(jié)果:', resolveRes)return getPromise(url) }).then(resolveRes => {console.log('請求結(jié)果:', resolveRes)return getPromise(url) })

經(jīng)過抽離之后,代碼看起來簡潔很多。同樣的,我們還是要再強(qiáng)調(diào)一下這個return問題。明明定義getPromise函數(shù)的時候已經(jīng)寫了return了,為什么后面再調(diào)用的時候還要再加return?因?yàn)楹瘮?shù)定義部分可以看到,調(diào)用函數(shù)之后返回的相當(dāng)于是一個new Promise,你可以對照之前寫的比較復(fù)雜的代碼,這個new Promise還需要再返回一下才行。

需要注意的一點(diǎn)是,在上面的例子中,then是平級的,也就是說,即使前一層請求失敗,也不會影響后一層的then執(zhí)行。如果你想對不同的請求失敗做統(tǒng)一的處理,可以這樣寫代碼:

getPromise(url).then(resolveRes => {console.log('請求結(jié)果:', resolveRes)return getPromise(url) }).then(resolveRes => {console.log('請求結(jié)果:', resolveRes)return getPromise(url) }).then(resolveRes => {console.log('請求結(jié)果:', resolveRes)return getPromise(url) }).catch(err => {console.log(err) })

這樣的話,只要有一個請求失敗,就會直接觸發(fā)catch,別的then都不會被執(zhí)行了。

此外,Promise還有一些靜態(tài)方法,我這里就不演示了。具體可以看MDN的Promise文檔。


async / await

除了使用Promise之外,我們還可以使用async/await來實(shí)現(xiàn)異步編程。

async關(guān)鍵字用于聲明異步函數(shù)。這個關(guān)鍵字可以用在函數(shù)聲明、函數(shù)表達(dá)式、箭頭函數(shù)和方法上。此外需要注意的是,如同我們在介紹Promise中強(qiáng)調(diào)的一樣,雖然async關(guān)鍵字可以讓函數(shù)具有異步特征,但里面的同步代碼還是會同步執(zhí)行。只有遇到異步執(zhí)行的函數(shù)時才會異步執(zhí)行。

async的函數(shù)在執(zhí)行后都會自動返回一個Promise對象,所以,我們也可以在async函數(shù)后面接then來做處理。不過我們還有一種更好的方式是使用await關(guān)鍵字。await可以獲取后面Promise對象成功狀態(tài)傳遞出來的參數(shù)。

我們下面舉一個具體例子,還是調(diào)用之前在講ajax部分定義的函數(shù)。

function getPromise(url) {return new Promise((resolve, reject) => {ajax(url, res => {console.log('獲得的響應(yīng)結(jié)果是', res)})resolve('成功')}) }// 改造后 function getPromise(url) {return new Promise((resolve, reject) => {ajax(url, res => {resolve(res)})}) }

我們把之前定義的getPromise函數(shù)略微改造一下。然后還是對之前的客戶端向服務(wù)端多次請求數(shù)據(jù)的功能使用async/await用代碼改造一下:

async function getData(){const res1 = await getPromise(url)console.log(res1)const res2 = await getPromise(url)console.log(res2)const res3 = await getPromise(url)console.log(res3) } getData()

通過await等待,我們會在每次得到數(shù)據(jù)之后再發(fā)送下一次請求。這樣異步編程看起來就像同步代碼了,使代碼變得更容易理解。

因?yàn)閍sync返回的是Promise,所以,對于失敗的請求,我們還是可以通過then的第二個參數(shù)或者catch來捕獲:

getData().catch(err => {console.log(err) })

參考資料

[1] JavaScript異步編程
[2] 說一說javascript的異步編程
[3] 線程進(jìn)程 |兩個簡單例子告訴你什么是進(jìn)程和線程 | 進(jìn)程線程原來如此簡單
[4] 并發(fā)與并行的區(qū)別是什么?
[5] 深入理解js事件循環(huán)機(jī)制(瀏覽器篇)
[6] JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop
[7] 《JavaScript高級程序設(shè)計(jì)(第4版)》
[8] 《深入淺出Nodejs》
[9] ajax
[10] Callback(回調(diào))是什么?

總結(jié)

以上是生活随笔為你收集整理的写给初学者的JavaScript异步编程和背后思想的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。