深刻理解nodejs中的異步編程

簡介

由於javascript默認狀況下是單線程的,這意味着代碼不能建立新的線程來並行執行。可是對於最開始在瀏覽器中運行的javascript來講,單線程的同步執行環境顯然沒法知足頁面點擊,鼠標移動這些響應用戶的功能。因而瀏覽器實現了一組API,可讓javascript以回調的方式來異步響應頁面的請求事件。javascript

更進一步,nodejs引入了非阻塞的 I/O ,從而將異步的概念擴展到了文件訪問、網絡調用等。java

今天,咱們將會深刻的探討一下各類異步編程的優缺點和發展趨勢。node

同步異步和阻塞非阻塞

在討論nodejs的異步編程以前,讓咱們來討論一個比較容易混淆的概念,那就是同步,異步,阻塞和非阻塞。編程

所謂阻塞和非阻塞是指進程或者線程在進行操做或者數據讀寫的時候,是否須要等待,在等待的過程當中可否進行其餘的操做。json

若是須要等待,而且等待過程當中線程或進程沒法進行其餘操做,只能傻傻的等待,那麼咱們就說這個操做是阻塞的。promise

反之,若是進程或者線程在進行操做或者數據讀寫的過程當中,還能夠進行其餘的操做,那麼咱們就說這個操做是非阻塞的。瀏覽器

同步和異步,是指訪問數據的方式,同步是指須要主動讀取數據,這個讀取過程多是阻塞或者是非阻塞的。而異步是指並不須要主動去讀取數據,是被動的通知。網絡

很明顯,javascript中的回調是一個被動的通知,咱們能夠稱之爲異步調用。異步

javascript中的回調

javascript中的回調是異步編程的一個很是典型的例子:async

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有兩個特色:

  1. 對象的狀態不受外界影響。

Promise對象表明一個異步操做,有三種狀態:Pending(進行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失敗)。

只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。

  1. 一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。

Promise對象的狀態改變,只有兩種可能:從Pending變爲Resolved和從Pending變爲Rejected。

這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的。

Promise的優勢

Promise將異步操做以同步操做的流程表達出來,避免了層層嵌套的回調函數。

Promise對象提供統一的接口,使得控制異步操做更加容易。

Promise的缺點

  1. 沒法取消Promise,一旦新建它就會當即執行,沒法中途取消。
  2. 若是不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。
  3. 當處於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:

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的博客

歡迎關注個人公衆號:「程序那些事」最通俗的解讀,最深入的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!

相關文章
相關標籤/搜索