首先明確一個問題,爲何 Node.js
須要異步編程?ajax
JavaScript
是單線程的,在發出一個調用時,在沒有獲得結果以前,該調用就不返回,意思就是調用者主動等待調用結果,換句話說,就是必須等待上一個任務執行完才能執行下一個任務,這種執行模式叫:同步。Node.js
的主要應用場景是處理高併發(單位時間內極大的訪問量)和 I/O
密集場景(ps: I/O
操做每每很是耗時,因此異步的關鍵在於解決 I/O
耗時問題),若是採用同步編程,問題就來了,服務器處理一個 I/O
請求須要大量的時間,後面的請求都將排隊,形成瀏覽器端的卡頓。異步編程能解決這個問題。
所謂異步,就是調用在發出後,這個調用就直接返回了,調用者不會當即獲得結果,可是不會阻塞,能夠繼續執行後續操做,而被調用者執行獲得結果後經過狀態、事件來通知調用者使用回調函數( callback
)來處理這個結果。Node在處理耗時的 I/O
操做時,將其交給其餘線程處理,本身繼續處理其餘訪問請求,當 I/O
操做處理好後就會經過事件通知 Node 用回調作後續處理。
有個例子很是好:編程
你打電話問書店老闆有沒有《分佈式系統》這本書,若是是同步通訊機制,書店老闆會說,你稍等,」我查一下",而後開始查啊查,等查好了(多是5秒,也多是一天)告訴你結果(返回結果)。而異步通訊機制,書店老闆直接告訴你我查一下啊,查好了打電話給你,而後直接掛電話了(不返回結果)。而後查好了,他會主動打電話給你。在這裏老闆經過「回電」這種方式來回調。
下面幾種方式是異步解決方案的進化過程:segmentfault
回調函數就是函數A做爲參數傳遞給函數B,而且在將來某一個時間被調用。callback的異步模式最大的問題就是,理解困難加回調地獄(callback hell),看下面的代碼的執行順序:promise
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); } D(); }); E();
其執行順序爲:A => E => B => D => C
,這種執行順序的確會讓人頭腦發昏,另外因爲因爲多個異步操做之間每每會耦合,只要中間一個操做須要修改,那麼它的上層回調函數和下層回調函數均可能要修改,這就陷入了回調地獄。而 Promise 對象就很好的解決了異步操做之間的耦合問題,讓咱們能夠用同步編程的方式去寫異步操做。瀏覽器
Promise
對象是一個構造函數,用來生成promise
實例。Promise
表明一個異步操做,有三種狀態:pending,resolved
(異步操做成功由 pending
變爲 resolved
),rejected
(異步操做失敗由 pending
變爲 rejected
),一旦變爲後兩種狀態將不會再改變。Promise
對象做爲構造函數接受一個函數做爲參數,而這個函數又接受 resolve
和 reject
兩個函數作爲參數,這兩個函數是JS內置的,無需配置。resolve
函數在異步操做成功後調用,將pending
狀態變爲resolved
,並將它的參數傳遞給回調函數;reject
函數在異步操做失敗時調用,將pending
狀態變爲rejected
,並將參數傳遞給回調函數。服務器
Promise
構造函數的原型上有一個then
方法,它接受兩個函數做爲參數,分別是 resolved
狀態和 rejected
狀態的回調函數,而這兩個回調函數接受的參數分別是Promise
實例中resolve
函數和reject
函數中的參數。 另外rejected
狀態的回調函數是可省略的。併發
下面是一個使用示例:異步
const instance = new Promise((resolve, reject) => { // 一些異步操做 if(/*異步操做成功*/) { resolve(value); } else { reject(error); } } }) instance.then(value => { // do something... }, error => { // do something... })
注意Promise實例在生成後會當即執行,而 then
方法只有在全部同步任務執行完後纔會執行,看看下面的例子:async
const promise = new Promise((resolve, reject) => { console.log('async task begins!'); setTimeout(() => { resolve('done, pending -> resolved!'); }, 1000); }) promise.then(value => { console.log(value); }) console.log('1.please wait'); console.log('2.please wait'); console.log('3.please wait'); // async task begins! // 1.please wait // 2.please wait // 3.please wait // done, pending -> resolved!
上面的實例能夠看出,Promise實例生成後當即執行,因此首先輸出 'async task begins!'
,隨後定義了一個異步操做 setTimeout
,1秒後執行,因此無需等待,向下執行,而then
方法指定的回調函數要在全部同步任務執行完後才執行,因此先輸出了3個'please wait'
,最後輸出'done, pending -> resolved!'
。(此處省略了then
方法中的reject
回調,通常不在then
中作rejected
狀態的處理,而使用catch
方法專門處理錯誤,至關於.then(null, reject)
)分佈式
then
方法會返回一個新的 Promise 實例,能夠分兩種狀況來看:
return new Promise(...)
,這種狀況沒啥好說的,因爲返回的是 Promise
,後面顯然能夠繼續調用then
方法。return 1
這種狀況仍是會返回一個 Promise
,而且這個Promise
當即執行回調 resolve(1)
。因此仍然能夠鏈式調用then
方法。(注:若是沒有指定return
語句,至關於返回了undefined
)使用 then 的鏈式寫法,按順序實現一系列的異步操做,這樣就能夠用同步編程的形式去實現異步操做,來看下面的例子,實現隔兩秒打一次招呼:
function sayHi(name) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(name); }, 2000) }) } sayHi('張三') .then(name => { console.log(`你好, ${name}`); return sayHi('李四'); // 最終 resolved 函數中的參數將做爲值傳遞給下一個then }) // name 是上一個then傳遞出來的參數 .then(name => { console.log(`你好, ${name}`); return sayHi('王二麻子'); }) .then(name => { console.log(`你好, ${name}`); }) // 你好, 張三 // 你好, 李四 // 你好, 王二麻子
能夠看到使用鏈式then的寫法,將異步操做變成了同步的形式,可是也帶來了新的問題,就是異步操做變成了很長的then鏈,新的解決方法就是Generator
,這裏跨過它直接說它的語法糖:async/await
。
async/await
其實是Generator
的語法糖。顧名思義,async
關鍵字表明後面的函數中有異步操做,await
表示等待一個異步方法執行完成。聲明異步函數只需在普通函數前面加一個關鍵字async
便可,如:
async function funcA() {}
async
函數返回一個Promise對象(若是指定的返回值不是Promise對象,也返回一個Promise,只不過當即 resolve
,處理方式同 then
方法),所以 async
函數經過 return
返回的值,會成爲 then
方法中回調函數的參數:
async function funcA() { return 'hello!'; } funcA().then(value => { console.log(value); }) // hello!
單獨一個 async
函數,其實與Promise執行的功能是同樣的,來看看 await
都幹了些啥。
顧名思義, await
就是異步等待,它等待的是一個Promise,所以 await
後面應該寫一個Promise對象,若是不是Promise對象,那麼會被轉成一個當即 resolve
的Promise。 async
函數被調用後就當即執行,可是一旦遇到 await
就會先返回,等到異步操做執行完成,再接着執行函數體內後面的語句。總結一下就是:async
函數調用不會形成代碼的阻塞,可是await
會引發async
函數內部代碼的阻塞。看看下面這個例子:
async function func() { console.log('async function is running!'); const num1 = await 200; console.log(`num1 is ${num1}`); const num2 = await num1+ 100; console.log(`num2 is ${num2}`); const num3 = await num2 + 100; console.log(`num3 is ${num3}`); } func(); console.log('run me before await!'); // async function is running! // run me before await! // num1 is 200 // num2 is 300 // num3 is 400
能夠看出調用 async func
函數後,它會當即執行,首先輸出了'async function is running!'
,接着遇到了 await
異步等待,函數返回,先執行func()
後面的同步任務,同步任務執行完後,接着await等待的位置繼續往下執行。能夠說,async
函數徹底能夠看做多個異步操做,包裝成的一個Promise 對象,而await
命令就是內部then
命令的語法糖。
值得注意的是, await
後面的 Promise 對象不老是返回 resolved
狀態,只要一個 await
後面的Promise狀態變爲 rejected
,整個 async
函數都會中斷執行,爲了保存錯誤的位置和錯誤信息,咱們須要用 try...catch
語句來封裝多個 await
過程,以下:
async function func() { try { const num1 = await 200; console.log(`num1 is ${num1}`); const num2 = await Promise.reject('num2 is wrong!'); console.log(`num2 is ${num2}`); const num3 = await num2 + 100; console.log(`num3 is ${num3}`); } catch (error) { console.log(error); } } func(); // num1 is 200 // 出錯了 // num2 is wrong!
如上所示,在 num2
處 await
獲得了一個狀態爲 rejected
的Promise對象,該錯誤會被傳遞到 catch
語句中,這樣咱們就能夠定位錯誤發生的位置。
接下來咱們用async/await
改寫一下Promise章節中關於sayHi
的一個例子,代碼以下:
function sayHi(name) { return new Promise((resolved, rejected) => { setTimeout(() => { resolved(name); }, 2000) }) } async function sayHi_async(name) { const sayHi_1 = await sayHi(name) console.log(`你好, ${sayHi_1}`) const sayHi_2 = await sayHi('李四') console.log(`你好, ${sayHi_2}`) const sayHi_3 = await sayHi('王二麻子') console.log(`你好, ${sayHi_3}`) } sayHi_async('張三') // 你好, 張三 // 你好, 李四 // 你好, 王二麻子
與以前長長的then鏈和then方法裏的回調函數相比,這樣的寫法看起來像是同步寫法而且更加清爽,更加符合編程習慣。
https://segmentfault.com/a/11...
https://segmentfault.com/a/11...
https://www.zhihu.com/questio...