這就是所謂的JavaScript非同步!
ECMAScript 6(簡稱ES6)將 JavaScript 非同步程式設計帶入了一個全新的階段。這篇文章的主題,就是介紹更強大、更完善的 ES6 非同步程式設計方法。
首先我們回顧一下javascript非同步的發展歷程。
ES6 以前:
回撥函式(callback):nodejs express 中常用,ajax中常用。
ES6:
promise物件:nodejs最早有bluebird promise的雛形,axios中常用。
generator函式:nodejs koa框架使用率很高。
ES7:
async/await語法:當前最常用的非同步語法,nodejs koa2 完全使用該語法。
什麼是非同步
所謂"非同步",簡單說就是一個任務分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。比如,有一個任務是讀取檔案進行處理,非同步的執行過程就是下面這樣。

非同步
上圖中,任務的第一段是向作業系統發出請求,要求讀取檔案。然後,程式執行其他任務,等到作業系統返回檔案,再接著執行任務的第二段(處理檔案)。
這種不連續的執行,就叫做非同步。相應地,連續的執行,就叫做同步。

同步
上圖就是同步的執行方式。由於是連續執行,不能插入其他任務,所以作業系統從硬碟讀取檔案的這段時間,程式只能乾等著。
回撥函式callback
JavaScript 語言對非同步程式設計的實現,就是回撥函式。所謂回撥函式,就是把任務的第二段單獨寫在一個函式裡面,等到重新執行這個任務的時候,就直接呼叫這個函式。它的英語名字 callback,直譯過來就是"重新呼叫"。
回撥字面也好理解,就是先處理本體函式,再處理回撥的函式,舉個例子,方便大家理解。

上面的例子很好理解,首先執行主體函式A,列印結果:我是主題函式;
然後執行回撥函式callback 也就是B,列印結果:我是回撥函式。
promise物件
promise 物件用於一個非同步操作的最終完成(或最終失敗)及其結果的表示。
簡單地說就是處理一個非同步請求。我們經常會做些斷言,如果我贏了你就嫁給我,如果輸了我就嫁給你之類的斷言。
這就是promise的中文含義:斷言,一個成功,一個失敗。
舉個例子,方便大家理解:
promise建構函式的引數是一個函式,我們把它稱為處理器函式。
處理器函式接收兩個函式reslove和reject作為其引數,當非同步操作順利執行則執行reslove函式, 當非同步操作中發生異常時,則執行reject函式。
通過resolve傳入得的值,可以在then方法中獲取到,通過reject傳入的值可以在chatch方法中獲取到。
因為then和catch都返回一個相同的promise物件,所以可以進行鏈式呼叫。

Promise 的寫法只是回撥函式的改進,使用then方法以後,非同步任務的兩段執行看得更清楚了,除此以外,並無新意。
Promise 的最大問題是程式碼冗餘,原來的任務被Promise 包裝了一下,不管什麼操作,一眼看去都是一堆 then,原來的語義變得很不清楚。
那麼,有沒有更好的寫法呢?
協程
傳統的程式語言,早有非同步程式設計的解決方案(其實是多工的解決方案)。其中有一種叫做"協程"(coroutine),意思是多個執行緒互相協作,完成非同步任務。
協程有點像函式,又有點像執行緒。它的執行流程大致如下。
第一步,協程A開始執行。
第二步,協程A執行到一半,進入暫停,執行權轉移到協程B。
第三步,(一段時間後)協程B交還執行權。
第四步,協程A恢復執行。
上面流程的協程A,就是非同步任務,因為它分成兩段(或多段)執行。
舉例來說,讀取檔案的協程寫法如下。
function asnycJob() { // ...其他程式碼 var f = yield readFile(fileA); // ...其他程式碼 }
上面程式碼的函式 asyncJob 是一個協程,它的奧妙就在其中的 yield 命令。它表示執行到此處,執行權將交給其他協程。也就是說,yield命令是非同步兩個階段的分界線。
協程遇到 yield 命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行。它的最大優點,就是程式碼的寫法非常像同步操作,如果去除yield命令,簡直一模一樣。
Generator 函式
Generator 函式是協程在 ES6 的實現,最大特點就是可以交出函式的執行權(即暫停執行)。
function* gen(x){ var y = yield x + 2; return y; }
上面程式碼就是一個 Generator 函式。它不同於普通函式,是可以暫停執行的,所以函式名之前要加星號,以示區別。
整個 Generator 函式就是一個封裝的非同步任務,或者說是非同步任務的容器。非同步操作需要暫停的地方,都用 yield 語句註明。Generator 函式的執行方法如下。
var g = gen(1); g.next() // { value: 3, done: false } g.next() // { value: undefined, done: true }
上面程式碼中,呼叫 Generator 函式,會返回一個內部指標(即遍歷器 )g 。這是 Generator 函式不同於普通函式的另一個地方,即執行它不會返回結果,返回的是指標物件。呼叫指標 g 的 next 方法,會移動內部指標(即執行非同步任務的第一段),指向第一個遇到的 yield 語句,上例是執行到 x + 2 為止。
換言之,next 方法的作用是分階段執行 Generator 函式。每次呼叫 next 方法,會返回一個物件,表示當前階段的資訊( value 屬性和 done 屬性)。value 屬性是 yield 語句後面表示式的值,表示當前階段的值;done 屬性是一個布林值,表示 Generator 函式是否執行完畢,即是否還有下一個階段。
Generator 函式的用法
下面看看如何使用 Generator 函式,執行一個真實的非同步任務。
var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); }
上面程式碼中,Generator 函式封裝了一個非同步操作,該操作先讀取一個遠端介面,然後從 JSON 格式的資料解析資訊。就像前面說過的,這段程式碼非常像同步操作,除了加上了 yield 命令。
執行這段程式碼的方法如下。
var g = gen(); var result = g.next(); result.value.then( function(data){ return data.json(); }).then( function(data){ g.next(data); });
上面程式碼中,首先執行 Generator 函式,獲取遍歷器物件,然後使用 next 方法(第二行),執行非同步任務的第一階段。由於 Fetch 模組返回的是一個 Promise 物件,因此要用 then 方法呼叫下一個next 方法。
可以看到,雖然 Generator 函式將非同步操作表示得很簡潔,但是流程管理卻不方便(即何時執行第一階段、何時執行第二階段)。
async-await
async函式返回一個promise物件,如果在async函式中返回一個直接量,async會通過Promise.resolve封裝成Promise物件。
我們可以通過呼叫promise物件的then方法,獲取這個直接量。

那如過async函式不返回值,又會是怎麼樣呢?

await會暫停當前async的執行,await會阻塞程式碼的執行,直到await後的表示式處理完成,程式碼才能繼續往下執行。
await後的表示式既可以是一個Promise物件,也可以是任何要等待的值。
如果await等到的是一個 Promise 物件,await 就忙起來了,它會阻塞後面的程式碼,等著 Promise 物件 resolve,然後得到 resolve 的值,作為 await 表示式的運算結果。
上邊你看到阻塞一詞,不要驚慌,async/await只是一種語法糖,程式碼執行與多個callback巢狀呼叫沒有區別。
本質並不是同步程式碼,它只是讓你思考程式碼邏輯的時候能夠以同步的思維去思考,避開回調地獄。
簡而言之-async/await是以同步的思維去寫非同步的程式碼,所以async/await並不會影響node的併發數,大家可以大膽的應用到專案中去!
如果它等到的不是一個 Promise 物件,那 await 表示式的運算結果就是它等到的東西。
舉個例子,方便大家理解:
【責任編輯:龐桂玉 TEL:(010)68476606】