簡介
因為javascript預設情況下是單線程的,這意味着代碼不能建立新的線程來并行執行。但是對于最開始在浏覽器中運作的javascript來說,單線程的同步執行環境顯然無法滿足頁面點選,滑鼠移動這些響應使用者的功能。于是浏覽器實作了一組API,可以讓javascript以回調的方式來異步響應頁面的請求事件。
更進一步,nodejs引入了非阻塞的 I/O ,進而将異步的概念擴充到了檔案通路、網絡調用等。
今天,我們将會深入的探讨一下各種異步程式設計的優缺點和發展趨勢。
同步異步和阻塞非阻塞
在讨論nodejs的異步程式設計之前,讓我們來讨論一個比較容易混淆的概念,那就是同步,異步,阻塞和非阻塞。
所謂阻塞和非阻塞是指程序或者線程在進行操作或者資料讀寫的時候,是否需要等待,在等待的過程中能否進行其他的操作。
如果需要等待,并且等待過程中線程或程序無法進行其他操作,隻能傻傻的等待,那麼我們就說這個操作是阻塞的。
反之,如果程序或者線程在進行操作或者資料讀寫的過程中,還可以進行其他的操作,那麼我們就說這個操作是非阻塞的。
同步和異步,是指通路資料的方式,同步是指需要主動讀取資料,這個讀取過程可能是阻塞或者是非阻塞的。而異步是指并不需要主動去讀取資料,是被動的通知。
很明顯,javascript中的回調是一個被動的通知,我們可以稱之為異步調用。
javascript中的回調
javascript中的回調是異步程式設計的一個非常典型的例子:
document.getElementById('button').addEventListener('click', () => {
console.log('button clicked!');
})
上面的代碼中,我們為button添加了一個click事件監聽器,如果監聽到了click事件,則會出發回調函數,輸出相應的資訊。
回調函數就是一個普通的函數,隻不過它被作為參數傳遞給了addEventListener,并且隻有事件觸發的時候才會被調用。
上篇文章我們講到的setTimeout和setInterval實際上都是異步的回調函數。
回調函數的錯誤處理
在nodejs中怎麼處理回調的錯誤資訊呢?nodejs采用了一個非常巧妙的辦法,在nodejs中,任何回調函數中的第一個參數為錯誤對象,我們可以通過判斷這個錯誤對象的存在與否,來進行相應的錯誤處理。
fs.readFile('/檔案.json', (err, data) => {
if (err !== null) {
//處理錯誤
console.log(err)
return
}
//沒有錯誤,則處理資料。
console.log(data)
})
回調地獄
javascript的回調雖然非常的優秀,它有效的解決了同步處理的問題。但是遺憾的是,如果我們需要依賴回調函數的傳回值來進行下一步的操作的時候,就會陷入這個回調地獄。
叫回調地獄有點誇張了,但是也是從一方面反映了回調函數所存在的問題。
fs.readFile('/a.json', (err, data) => {
if (err !== null) {
fs.readFile('/b.json',(err,data) =>{
//callback inside callback
})
}
})
怎麼解決呢?
别怕ES6引入了Promise,ES2017引入了Async/Await都可以解決這個問題。
ES6中的Promise
什麼是Promise
Promise 是異步程式設計的一種解決方案,比傳統的解決方案“回調函數和事件”更合理和更強大。
所謂Promise,簡單說就是一個容器,裡面儲存着某個未來才會結束的事件(通常是一個異步操作)的結果。
從文法上說,Promise 是一個對象,從它可以擷取異步操作的消息。
Promise的特點
Promise有兩個特點:
- 對象的狀态不受外界影響。
Promise對象代表一個異步操作,有三種狀态:Pending(進行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失敗)。
隻有異步操作的結果,可以決定目前是哪一種狀态,任何其他操作都無法改變這個狀态。
- 一旦狀态改變,就不會再變,任何時候都可以得到這個結果。
Promise對象的狀态改變,隻有兩種可能:從Pending變為Resolved和從Pending變為Rejected。
這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
Promise的優點
Promise将異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。
Promise對象提供統一的接口,使得控制異步操作更加容易。
Promise的缺點
- 無法取消Promise,一旦建立它就會立即執行,無法中途取消。
- 如果不設定回調函數,Promise内部抛出的錯誤,不會反應到外部。
- 當處于Pending狀态時,無法得知目前進展到哪一個階段(剛剛開始還是即将完成)。
Promise的用法
Promise對象是一個構造函數,用來生成Promise執行個體:
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操作成功 */){
resolve(value);
} else { reject(error); }
}
);
promise可以接then操作,then操作可以接兩個function參數,第一個function的參數就是建構Promise的時候resolve的value,第二個function的參數就是建構Promise的reject的error。
promise.then(function(value) {
// success
}, function(error) {
// failure }
);
我們看一個具體的例子:
function timeout(ms){
return new Promise(((resolve, reject) => {
setTimeout(resolve,ms,'done');
}))
}
timeout(100).then(value => console.log(value));
Promise中調用了一個setTimeout方法,并會定時觸發resolve方法,并傳入參數done。
最後程式輸出done。
Promise的執行順序
Promise一經建立就會立馬執行。但是Promise.then中的方法,則會等到一個調用周期過後再次調用,我們看下面的例子:
let promise = new Promise(((resolve, reject) => {
console.log('Step1');
resolve();
}));
promise.then(() => {
console.log('Step3');
});
console.log('Step2');
輸出:
Step1
Step2
Step3
async和await
Promise當然很好,我們将回調地獄轉換成了鍊式調用。我們用then來将多個Promise連接配接起來,前一個promise resolve的結果是下一個promise中then的參數。
鍊式調用有什麼缺點呢?
比如我們從一個promise中,resolve了一個值,我們需要根據這個值來進行一些業務邏輯的處理。
假如這個業務邏輯很長,我們就需要在下一個then中寫很長的業務邏輯代碼。這樣讓我們的代碼看起來非常的備援。
那麼有沒有什麼辦法可以直接傳回promise中resolve的結果呢?
答案就是await。
當promise前面加上await的時候,調用的代碼就會停止直到 promise 被解決或被拒絕。
注意await一定要放在async函數中,我們來看一個async和await的例子:
const logAsync = () => {
return new Promise(resolve => {
setTimeout(() => resolve('小馬哥'), 5000)
})
}
上面我們定義了一個logAsync函數,該函數傳回一個Promise,因為該Promise内部使用了setTimeout來resolve,是以我們可以将其看成是異步的。
要是使用await得到resolve的值,我們需要将其放在一個async的函數中:
const doSomething = async () => {
const resolveValue = await logAsync();
console.log(resolveValue);
}
async的執行順序
await實際上是去等待promise的resolve結果我們把上面的例子結合起來:
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是異步執行的,并且它的順序是在目前這個周期之後。
async的特點
async會讓所有後面接的函數都變成Promise,即使後面的函數沒有顯示的傳回Promise。
const asyncReturn = async () => {
return 'async return'
}
asyncReturn().then(console.log)
因為隻有Promise才能在後面接then,我們可以看出async将一個普通的函數封裝成了一個Promise:
const asyncReturn = async () => {
return Promise.resolve('async return')
}
asyncReturn().then(console.log)
總結
promise避免了回調地獄,它将callback inside callback改寫成了then的鍊式調用形式。
但是鍊式調用并不友善閱讀和調試。于是出現了async和await。
async和await将鍊式調用改成了類似程式順序執行的文法,進而更加友善了解和調試。
我們來看一個對比,先看下使用Promise的情況:
const getUserInfo = () => {
return fetch('/users.json') // 擷取使用者清單
.then(response => response.json()) // 解析 JSON
.then(users => users[0]) // 選擇第一個使用者
.then(user => fetch(`/users/${user.name}`)) // 擷取使用者資料
.then(userResponse => userResponse.json()) // 解析 JSON
}
getUserInfo()
~~~
将其改寫成async和await:
~~~js
const getUserInfo = async () => {
const response = await fetch('/users.json') // 擷取使用者清單
const users = await response.json() // 解析 JSON
const user = users[0] // 選擇第一個使用者
const userResponse = await fetch(`/users/${user.name}`) // 擷取使用者資料
const userData = await userResponse.json() // 解析 JSON
return userData
}
getUserInfo()
可以看到業務邏輯變得更加清晰。同時,我們擷取到了很多中間值,這樣也友善我們進行調試。
本文作者:flydean程式那些事
本文連結:
http://www.flydean.com/nodejs-async/本文來源:flydean的部落格
歡迎關注我的公衆号:「程式那些事」最通俗的解讀,最深刻的幹貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!