有一個有趣的問題:node
爲何
Node.js
約定回調函數的第一個參數必須是錯誤對象err
(若是沒有錯誤,該參數就是null
)?git
緣由是執行回調函數對應的異步操做,它的執行分紅兩段,這兩段之間拋出的錯誤程序沒法捕獲,因此只能做爲參數傳入第二段。你們知道,JavaScript
只有一個線程,若是沒有異步編輯,複雜的程序基本無法使用。在ES6誕生之前,異步編程的方式大概有下面四種:github
回調函數編程
事件監聽json
發佈/訂閱api
Promise
對象promise
ES6將JavaScript
異步編程帶入了一個全新的階段,ES7中的async
函數更是給出了異步編程的終極解決方案。下面將具體講解異步編程的原理和值得注意的地方,待我細細道來~babel
所謂異步
,簡單地說就是一個任務分紅兩段,先執行第一段,而後轉而執行其餘任務,等作好準備再回過頭執行第二段。併發
舉個例子
讀取一個文件進行處理,任務的第一段是向操做系統發出請求,要求讀取文件。而後,程序執行其餘任務,等到操做系統返回文件,再接着執行任務的第二段(處理文件)。這種不連續的執行,就叫作異步。異步
相應地,連續的執行就叫做同步。因爲是連續執行,不能插入其餘任務,因此操做系統從硬盤讀取文件的這段時間,程序只能乾等着。
所謂回調函數,就是把任務的第二段單獨寫在一個函數中,等到從新執行該任務時直接調用這個函數。其英文名字 callback
直譯過來就是 "從新調用"的意思。
拿上面的例子講,讀取文件操做是這樣的:
fs.readFile(fileA, (err, data) => { if (err) throw err; console.log(data) }) fs.readFile(fileB, (err, data) => { if (err) throw err; console.log(data) })
注意:上面兩段代碼彼此是異步的,雖然開始執行的順序是從上到下,可是第二段並不會等到第一段結束才執行,而是併發執行。
那麼問題來了,若是想fileB
等到fileA
讀取成功後再開始執行應該怎麼處理呢?最簡單的辦法是經過 回調嵌套:
fs.readFile(fileA, (err, data) => { if (err) throw err; console.log(data) fs.readFile(fileB, (_err, _data) => { if (_err) throw err; console.log(_data) }) })
這種方式我只能容忍個位數字的嵌套,並且它使得代碼橫向發展,實在是醜的一筆,次數多了根本是無法看。試想萬一要同步執行100個異步操做呢?瘋掉算了吧!有沒有更好的辦法呢?
Promise
要澄清一點,Promise
的概念並非ES6
新出的,而是ES6
整合了一套新的寫法。一樣繼續上面的例子,使用Promise
代碼就變成這樣了:
var readFile = require('fs-readfile-promise'); readFile(fileA) .then((data)=>{console.log(data)}) .then(()=>{return readFile(fileB)}) .then((data)=>{console.log(data)}) // ... 讀取n次 .catch((err)=>{console.log(err)})
注意:上面代碼使用了
Node
封裝好的Promise
版本的readFile
函數,它的原理其實就是返回一個Promise
對象,咱也簡單地寫一個:
var fs = require('fs'); var readFile = function(path) { return new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { if (err) reject(err) resolve(data) }) }) } module.export = readFile
可是,
Promise
的寫法只是回調函數的改進,使用then()
以後,異步任務的兩段執行看得更清楚,除此以外並沒有新意。撇開優勢,Promise
的最大問題就是代碼冗餘,原來的任務被Promise
包裝一下,無論什麼操做,一眼看上去都是一堆then()
,本來的語意變得很不清楚。
把酒問蒼天,MD還有更好的辦法嗎?
Generator
在引入generator
以前,先介紹一下什麼叫 協程
"攜程在手,說走就走"。哈哈,別混淆了, "協程" 非 "攜程"
所謂 "協程" ,就是多個線程相互協做,完成異步任務。協程有點像函數,又有點像線程。其運行流程大體以下:
第一步: 協程A開始執行
第二步:協程A執行到一半,暫停,執行權轉移到協程B
第三步:一段時間後,協程B交還執行權
第四步:協程A恢復執行
function asyncJob() { // ... 其餘代碼 var f = yield readFile(fileA); // ... 其餘代碼 }
上面的
asyncJob()
就是一個協程,它的奧妙就在於其中的yield
命令。它表示執行到此處執行權交給其餘協程,換而言之,yield
就是異步兩個階段的分界線。
協程遇到yield
命令就暫停,等到執行權返回,再從暫停的地方繼續日後執行。它的最大優勢就是代碼的寫法很是像同步操做,若是除去 yield
命令,簡直如出一轍。
Generator
函數Generator
函數是協程在ES6中的實現,最大的特色就是能夠交出函數的執行權(即暫停執行)。整個Generator
函數就是一個封裝的異步任務,或者說就是異步任務的容器。
function* gen(x) { var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next() // { value: undefined, done: true }
上面的代碼中,調用Generator
函數,會返回一個內部指針(即遍歷器)g,這是Generator
函數不一樣於普通函數的另外一個地方,即執行它不會返回結果,返回的是指針對象。調用指針g的next()
方法,會移動內部指針(即執行異步任務的第一段),指向第一個遇到的yield
語句。
換而言之,next()
方法的做用是分階段執行Generator
函數。每次調用next()
方法,會返回一個對象,表示當前階段的信息(value
屬性和done
屬性)。value
屬性是yield
語句後面表達式的值,表示當前階段的值;done
屬性是一個布爾值,表示Generator
函數是否執行完畢,便是否還有一個階段。
Generator
函數的數據交換和錯誤處理Generator
函數能夠暫停執行和恢復執行,這是它封裝異步任務的根本緣由。除此以外,它還有兩個特性,使它能夠做爲異步編程的解決方案:函數體內外的數據交換和錯誤處理機制。
next()
方法返回值的value
屬性,是Generator
函數向外輸出的數據;next()
方法還能夠接受參數,向Generator
函數體內輸入數據。
function* gen(x) { var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next(2) // { value: 2, done: true }
上面的代碼中,第一個
next()
方法的value
屬性,返回表達式x+2
的值(3)。第二個next()
方法帶有參數2,這個參數能夠傳入Generator
函數,做爲上個階段異步任務的返回結果,被函數體內的變量y接收,所以這一步的value
屬性返回的就是2(變量y的值)。
Generator
函數內部還能夠部署錯誤處理代碼,捕獲函數體外拋出的錯誤。
function* gen(x) { try { var y = yield x + 2 } catch(e) { console.log(e) } return y } var g = gen(1); g.next(); g.throw('出錯了');
上面代碼的最後一行,Generator
函數體外,使用指針對象的throw
方法拋出的錯誤,能夠被函數體內的try...catch
代碼塊捕獲。這意味着,出錯的代碼與處理錯誤的代碼,實現了時間和空間上的分離,這對於異步編程無疑是很重要的。
下面看看如何使用Generator
函數,執行一個真實的異步任務。
var fetch = require('node-fetch') function* gen() { var url = 'https://api.github.com/usrs/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
函數所謂async
函數,實際上是Generator
函數的語法糖。
繼續咱們異步讀取文件的例子,使用Generator
實現
var fs = require('fs'); var readFile = (path) => { return new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { if (err) reject(err) resolve(data) }) }) } var gen = function* () { var f1 = yield readFile(fileA); var f2 = yield readFile(fileB); console.log(f1.toString()); console.log(f2.toString()); }
寫成async
函數,就是下面這樣:
var asyncReadFile = async function() { var f1 = await readFile(fileA); var f2 = await readFile(fileB); console.log(f1.toString()) console.log(f2.toString()) }
發現了吧,async
函數就是將Generator
函數的*
替換成了async
,將yield
替換成await
,除此以外,還對 Generator
作了如下四點改進:
(1)內置執行器。Generator
函數的執行好比靠執行器,因此纔有了co
模塊等異步執行器,而async
函數是自帶執行器的。也就是說:async
函數的執行,與普通函數如出一轍,只要一行:
var result = asyncReadFile();
(2)上面的代碼調用了asyncReadFile()
,就會自動執行,輸出最後結果。這徹底不像Generator
函數,須要調用next()
方法,或者使用co
模塊,才能獲得真正執行,從而獲得最終結果。
(3)更好的語義。async
和await
比起星號和yield
,語義更清楚。async
表示函數裏有異步操做,await
表示緊跟在後面的表達式須要等待結果。
(4)更廣的適用性。async
函數的await
命令後面能夠是Promise
對象和原始類型的值(數值、字符串和布爾值,而這是等同於同步操做)。
(5)返回值是Promise
,這比Generator
函數返回的是Iterator
對象方便多了。你能夠用then()
指定下一步操做。
進一步說,
async
函數徹底能夠看做由多個異步操做包裝成的一個Promise
對象,而await
命令就是內部then()
命令的語法糖。
async
函數的實現就是將Generator
函數和自動執行器包裝在一個函數中。以下代碼:
async function fn(args) { // ... } // 等同於 function fn(args) { return spawn(function*() { // ... }) } // 自動執行器 function spawn(genF) { return new Promise(function(resolve, reject) { var gen = genF(); function step(nextF) { try { var next = nextF() } catch(e) { return reject(e) } if (next.done) { return resolve(next.value) } Promise.resolve(next.value).then(function(v) { step(function() { return gen.next(v) }) },function(e) { step(function() { return gen.throw(e) }) }) } step(function() { return gen.next(undefined) }) }) }
async
函數用法(1)async
函數返回一個Promise
對象,能夠是then()
方法添加回調函數。
(2)當函數執行時,一旦遇到await()
就會先返回,等到觸發的異步操做完成,再接着執行函數體內後面的語句。
下面是一個延遲輸出結果的例子:
function timeout(ms) { return new Promise((resolve) => { setTimeout(resolve, ms) }) } async function asyncPrint(value, ms) { await timeout(ms) console.log(value) } // 延遲500ms後輸出 "Hello World!" asyncPrint('Hello World!', 500)
(1)await
命令後面的Promise
對象,運行結果多是reject
,因此最好把await
命令放在try...catch
代碼塊中。
(2)await
命令只能用在async
函數中,用在普通函數中會報錯。
(3)ES6
將await
增長爲保留字。若是使用這個詞做爲標識符,在ES5
中是合法的,可是ES6
會拋出 SyntaxError
(語法錯誤)。
"倚天不出誰與爭鋒",上面介紹了一大堆,最後仍是讓咱們經過一個例子來看看 async
函數和Promise
、Generator
到底誰纔是真正的老大吧!
需求:假定某個DOM元素上部署了一系列的動畫,前一個動畫結束才能開始後一個。若是當中又一個動畫出錯就再也不往下執行,返回上一個成功執行動畫的返回值。
Promise
實現function chainAnimationsPromise(ele, animations) { // 變量ret用來保存上一個動畫的返回值 var ret = null; // 新建一個空的Promise var p = Promise.resolve(); // 使用then方法添加全部動畫 for (var anim in animations) { p = p.then(function(val) { ret = val; return anim(ele); }) } // 返回一個部署了錯誤捕獲機制的Promise return p.catch(function(e) { /* 忽略錯誤,繼續執行 */ }).then(function() { return ret; }) }
雖然Promise
的寫法比起回調函數的寫法有很大的改進,可是操做自己的語義卻變得不太明朗。
Generator
實現function chainAnimationsGenerator(ele, animations) { return spawn(function*() { var ret = null; try { for(var anim of animations) { ret = yield anim(ele) } } catch(e) { /* 忽略錯誤,繼續執行 */ } return ret; }) }
使用Generator
雖然語義比Promise
寫法清晰很多,可是用戶定義的操做所有出如今spawn
函數的內部。這個寫法的問題在於,必須有一個任務運行器自動執行Generator
函數,它返回一個Promise
對象,並且保證yield
語句後的表達式返回的是一個Promise
。上面的spawn
就扮演了這一角色。它的實現以下:
function spawn(genF) { return new Promise(function(resolve, reject) { var gen = genF(); function step(nextF) { try { var next = nextF() } catch(e) { return reject(e) } if (next.done) { return resolve(next.value) } Promise.resolve(next.value).then(function(v) { step(function() { return gen.next(v) }) },function(e) { step(function() { return gen.throw(e) }) }) } step(function() { return gen.next(undefined) }) }) }
async
實現async function chainAnimationAsync(ele, animations) { var ret = null; try { for(var anim of animations) { ret = await anim(ele) } } catch(e) { /* 忽略錯誤,繼續執行 */ } return ret; }
好了,光從代碼量上就看出優點了吧!簡潔又符合語義,幾乎沒有不相關代碼。完勝!
注意一點:
async
屬於ES7的提案,使用時請經過babel
或者regenerator
進行轉碼。
阮一峯 《ES6標準入門》
@歡迎關注個人 github 和 我的博客 -Jafeney