一文秒懂nodejs中的異步編程
文章目錄 簡介同步異步和阻塞非阻塞javascript中的回調(diào)回調(diào)函數(shù)的錯(cuò)誤處理回調(diào)地獄 ES6中的Promise什么是PromisePromise的特點(diǎn)Promise的優(yōu)點(diǎn)Promise的缺點(diǎn)Promise的用法Promise的執(zhí)行順序 async和awaitasync的執(zhí)行順序async的特點(diǎn) 總結(jié)
簡介
因?yàn)閖avascript默認(rèn)情況下是單線程的,這意味著代碼不能創(chuàng)建新的線程來并行執(zhí)行。但是對(duì)于最開始在瀏覽器中運(yùn)行的javascript來說,單線程的同步執(zhí)行環(huán)境顯然無法滿足頁面點(diǎn)擊,鼠標(biāo)移動(dòng)這些響應(yīng)用戶的功能。于是瀏覽器實(shí)現(xiàn)了一組API,可以讓javascript以回調(diào)的方式來異步響應(yīng)頁面的請(qǐng)求事件。
更進(jìn)一步,nodejs引入了非阻塞的 I/O ,從而將異步的概念擴(kuò)展到了文件訪問、網(wǎng)絡(luò)調(diào)用等。
今天,我們將會(huì)深入的探討一下各種異步編程的優(yōu)缺點(diǎn)和發(fā)展趨勢。
同步異步和阻塞非阻塞
在討論nodejs的異步編程之前,讓我們來討論一個(gè)比較容易混淆的概念,那就是同步,異步,阻塞和非阻塞。
所謂阻塞和非阻塞是指進(jìn)程或者線程在進(jìn)行操作或者數(shù)據(jù)讀寫的時(shí)候,是否需要等待,在等待的過程中能否進(jìn)行其他的操作。
如果需要等待,并且等待過程中線程或進(jìn)程無法進(jìn)行其他操作,只能傻傻的等待,那么我們就說這個(gè)操作是阻塞的。
反之,如果進(jìn)程或者線程在進(jìn)行操作或者數(shù)據(jù)讀寫的過程中,還可以進(jìn)行其他的操作,那么我們就說這個(gè)操作是非阻塞的。
同步和異步,是指訪問數(shù)據(jù)的方式,同步是指需要主動(dòng)讀取數(shù)據(jù),這個(gè)讀取過程可能是阻塞或者是非阻塞的。而異步是指并不需要主動(dòng)去讀取數(shù)據(jù),是被動(dòng)的通知。
很明顯,javascript中的回調(diào)是一個(gè)被動(dòng)的通知,我們可以稱之為異步調(diào)用。
javascript中的回調(diào)
javascript中的回調(diào)是異步編程的一個(gè)非常典型的例子:
document.getElementById('button').addEventListener('click', () => { console.log('button clicked!'); })
上面的代碼中,我們?yōu)閎utton添加了一個(gè)click事件監(jiān)聽器,如果監(jiān)聽到了click事件,則會(huì)出發(fā)回調(diào)函數(shù),輸出相應(yīng)的信息。
回調(diào)函數(shù)就是一個(gè)普通的函數(shù),只不過它被作為參數(shù)傳遞給了addEventListener,并且只有事件觸發(fā)的時(shí)候才會(huì)被調(diào)用。
上篇文章我們講到的setTimeout和setInterval實(shí)際上都是異步的回調(diào)函數(shù)。
回調(diào)函數(shù)的錯(cuò)誤處理
在nodejs中怎么處理回調(diào)的錯(cuò)誤信息呢?nodejs采用了一個(gè)非常巧妙的辦法,在nodejs中,任何回調(diào)函數(shù)中的第一個(gè)參數(shù)為錯(cuò)誤對(duì)象,我們可以通過判斷這個(gè)錯(cuò)誤對(duì)象的存在與否,來進(jìn)行相應(yīng)的錯(cuò)誤處理。
fs.readFile('/文件.json', (err, data) => { if (err !== null) { //處理錯(cuò)誤 console.log(err) return } //沒有錯(cuò)誤,則處理數(shù)據(jù)。 console.log(data) })
回調(diào)地獄
javascript的回調(diào)雖然非常的優(yōu)秀,它有效的解決了同步處理的問題。但是遺憾的是,如果我們需要依賴回調(diào)函數(shù)的返回值來進(jìn)行下一步的操作的時(shí)候,就會(huì)陷入這個(gè)回調(diào)地獄。
叫回調(diào)地獄有點(diǎn)夸張了,但是也是從一方面反映了回調(diào)函數(shù)所存在的問題。
fs.readFile('/a.json', (err, data) => { if (err !== null) { fs.readFile('/b.json',(err,data) =>{ //callback inside callback }) } })
怎么解決呢?
別怕ES6引入了Promise,ES2017引入了Async/Await都可以解決這個(gè)問題。
ES6中的Promise
什么是Promise
Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案“回調(diào)函數(shù)和事件”更合理和更強(qiáng)大。
所謂Promise,簡單說就是一個(gè)容器,里面保存著某個(gè)未來才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果。
從語法上說,Promise 是一個(gè)對(duì)象,從它可以獲取異步操作的消息。
Promise的特點(diǎn)
Promise有兩個(gè)特點(diǎn):
對(duì)象的狀態(tài)不受外界影響。
Promise對(duì)象代表一個(gè)異步操作,有三種狀態(tài):Pending(進(jìn)行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失?。?。
只有異步操作的結(jié)果,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無法改變這個(gè)狀態(tài)。
一旦狀態(tài)改變,就不會(huì)再變,任何時(shí)候都可以得到這個(gè)結(jié)果。
Promise對(duì)象的狀態(tài)改變,只有兩種可能:從Pending變?yōu)镽esolved和從Pending變?yōu)镽ejected。
這與事件(Event)完全不同,事件的特點(diǎn)是,如果你錯(cuò)過了它,再去監(jiān)聽,是得不到結(jié)果的。
Promise的優(yōu)點(diǎn)
Promise將異步操作以同步操作的流程表達(dá)出來,避免了層層嵌套的回調(diào)函數(shù)。
Promise對(duì)象提供統(tǒng)一的接口,使得控制異步操作更加容易。
Promise的缺點(diǎn)
- 無法取消Promise,一旦新建它就會(huì)立即執(zhí)行,無法中途取消。
- 如果不設(shè)置回調(diào)函數(shù),Promise內(nèi)部拋出的錯(cuò)誤,不會(huì)反應(yīng)到外部。
- 當(dāng)處于Pending狀態(tài)時(shí),無法得知目前進(jìn)展到哪一個(gè)階段(剛剛開始還是即將完成)。
Promise的用法
Promise對(duì)象是一個(gè)構(gòu)造函數(shù),用來生成Promise實(shí)例:
var promise = new Promise(function(resolve, reject) { // ... some code if (/* 異步操作成功 */){ resolve(value); } else { reject(error); } } );
promise可以接then操作,then操作可以接兩個(gè)function參數(shù),第一個(gè)function的參數(shù)就是構(gòu)建Promise的時(shí)候resolve的value,第二個(gè)function的參數(shù)就是構(gòu)建Promise的reject的error。
promise.then(function(value) { // success }, function(error) { // failure } );
我們看一個(gè)具體的例子:
function timeout(ms){ return new Promise(((resolve, reject) => { setTimeout(resolve,ms,'done'); })) } timeout(100).then(value => console.log(value));
Promise中調(diào)用了一個(gè)setTimeout方法,并會(huì)定時(shí)觸發(fā)resolve方法,并傳入?yún)?shù)done。
最后程序輸出done。
Promise的執(zhí)行順序
Promise一經(jīng)創(chuàng)建就會(huì)立馬執(zhí)行。但是Promise.then中的方法,則會(huì)等到一個(gè)調(diào)用周期過后再次調(diào)用,我們看下面的例子:
let promise = new Promise(((resolve, reject) => { console.log('Step1'); resolve(); })); promise.then(() => { console.log('Step3'); }); console.log('Step2'); 輸出: Step1 Step2 Step3
async和await
Promise當(dāng)然很好,我們將回調(diào)地獄轉(zhuǎn)換成了鏈?zhǔn)秸{(diào)用。我們用then來將多個(gè)Promise連接起來,前一個(gè)promise resolve的結(jié)果是下一個(gè)promise中then的參數(shù)。
鏈?zhǔn)秸{(diào)用有什么缺點(diǎn)呢?
比如我們從一個(gè)promise中,resolve了一個(gè)值,我們需要根據(jù)這個(gè)值來進(jìn)行一些業(yè)務(wù)邏輯的處理。
假如這個(gè)業(yè)務(wù)邏輯很長,我們就需要在下一個(gè)then中寫很長的業(yè)務(wù)邏輯代碼。這樣讓我們的代碼看起來非常的冗余。
那么有沒有什么辦法可以直接返回promise中resolve的結(jié)果呢?
答案就是await。
當(dāng)promise前面加上await的時(shí)候,調(diào)用的代碼就會(huì)停止直到 promise 被解決或被拒絕。
注意await一定要放在async函數(shù)中,我們來看一個(gè)async和await的例子:
const logAsync = () => { return new Promise(resolve => { setTimeout(() => resolve('小馬哥'), 5000) }) }
上面我們定義了一個(gè)logAsync函數(shù),該函數(shù)返回一個(gè)Promise,因?yàn)樵揚(yáng)romise內(nèi)部使用了setTimeout來resolve,所以我們可以將其看成是異步的。
要是使用await得到resolve的值,我們需要將其放在一個(gè)async的函數(shù)中:
const doSomething = async () => { const resolveValue = await logAsync(); console.log(resolveValue); }
async的執(zhí)行順序
await實(shí)際上是去等待promise的resolve結(jié)果我們把上面的例子結(jié)合起來:
const logAsync = () => { return new Promise(resolve => { setTimeout(() => resolve('小馬哥'), 1000) }) } const doSomething = async () => { const resolveValue = await logAsync(); console.log(resolveValue); } console.log('before') doSomething(); console.log('after')
上面的例子輸出:
before
after
小馬哥
可以看到,aysnc是異步執(zhí)行的,并且它的順序是在當(dāng)前這個(gè)周期之后。
async的特點(diǎn)
async會(huì)讓所有后面接的函數(shù)都變成Promise,即使后面的函數(shù)沒有顯示的返回Promise。
const asyncReturn = async () => { return 'async return' } asyncReturn().then(console.log)
因?yàn)橹挥蠵romise才能在后面接then,我們可以看出async將一個(gè)普通的函數(shù)封裝成了一個(gè)Promise:
const asyncReturn = async () => { return Promise.resolve('async return') } asyncReturn().then(console.log)
總結(jié)
promise避免了回調(diào)地獄,它將callback inside callback改寫成了then的鏈?zhǔn)秸{(diào)用形式。
但是鏈?zhǔn)秸{(diào)用并不方便閱讀和調(diào)試。于是出現(xiàn)了async和await。
async和await將鏈?zhǔn)秸{(diào)用改成了類似程序順序執(zhí)行的語法,從而更加方便理解和調(diào)試。
我們來看一個(gè)對(duì)比,先看下使用Promise的情況:
const getUserInfo = () => { return fetch('/users.json') // 獲取用戶列表 .then(response => response.json()) // 解析 JSON .then(users => users[0]) // 選擇第一個(gè)用戶 .then(user => fetch(`/users/${user.name}`)) // 獲取用戶數(shù)據(jù) .then(userResponse => userResponse.json()) // 解析 JSON } getUserInfo()
將其改寫成async和await:
const getUserInfo = async () => { const response = await fetch('/users.json') // 獲取用戶列表 const users = await response.json() // 解析 JSON const user = users[0] // 選擇第一個(gè)用戶 const userResponse = await fetch(`/users/${user.name}`) // 獲取用戶數(shù)據(jù) const userData = await userResponse.json() // 解析 JSON return userData } getUserInfo()
可以看到業(yè)務(wù)邏輯變得更加清晰。同時(shí),我們獲取到了很多中間值,這樣也方便我們進(jìn)行調(diào)試。
到此這篇關(guān)于深入理解nodejs中的異步編程的文章就介紹到這了,更多相關(guān)nodejs異步編程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
node.js 中間件express-session使用詳解
這篇文章主要給大家介紹了node.js中間件express-session使用的相關(guān)資料,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-05-05基于Node.js + WebSocket打造即時(shí)聊天程序嗨聊
這篇文章主要介紹了基于Node.js + WebSocket打造即時(shí)聊天程序,有興趣的可以了解一下。2016-11-11使用pkg打包nodejs項(xiàng)目并解決本地文件讀取的問題
這篇文章主要介紹了使用pkg打包nodejs項(xiàng)目并解決本地文件讀取的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10Node.js創(chuàng)建Web、TCP服務(wù)器
這篇文章主要介紹了用Node.js創(chuàng)建Web服務(wù)器和TCP服務(wù)器的方法和處理技巧,需要的讀者們學(xué)習(xí)一下吧。2017-12-12Node.js中console.log()輸出彩色字體的方法示例
這篇文章主要給大家介紹了關(guān)于Node.js中console.log()輸出彩色字體的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Node.js具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12Node.js重新刷新session過期時(shí)間的方法
在Node.js中,我們通常使用express-session這個(gè)包來使用和管理session,保存服務(wù)端和客戶端瀏覽器之間的會(huì)話狀態(tài)。那如何才能實(shí)現(xiàn)當(dāng)用戶刷新當(dāng)前頁面或者點(diǎn)擊頁面上的按鈕時(shí)重新刷新session的過期時(shí)間呢,接下來通過本文一起學(xué)習(xí)吧2016-02-02