異步編程是每個使用 JavaScript 編程的人都會遇到的問題,無論是前端的 ajax 請求,或是 node 的各種異步 API。本文就來總結(jié)一下常見的四種處理異步編程的方法。
一、回調(diào)函數(shù)
使用回調(diào)函數(shù)是最常見的一種形式,下面來舉幾個例子:
// jQuery ajax
$.get('test.html', data => {
$('#result').html(data)
})
// node 異步讀取文件
const fs = require('fs')
fs.readFile('/etc/passwd', (err, data) => {
if (err) {
throw err
}
console.log(data)
})
回調(diào)函數(shù)非常容易理解,就是定義函數(shù)的時候?qū)⒘硪粋€函數(shù)(回調(diào)函數(shù))作為參數(shù)傳入定義的函數(shù)當(dāng)中,當(dāng)異步操作執(zhí)行完畢后在執(zhí)行該回調(diào)函數(shù),從而可以確保接下來的操作在異步操作之后執(zhí)行。
回調(diào)函數(shù)的缺點在于當(dāng)需要執(zhí)行多個異步操作的時候會將多個回調(diào)函數(shù)嵌套在一起,組成代碼結(jié)構(gòu)上混亂,被稱為回調(diào)地獄(callback hell)。
func1(data0, data1 => {
func2(data2, data3 => {
func3(data3, data4 => data4)
})
})
二、Promise
Promise 利用一種鏈式調(diào)用的方法來組織異步代碼,可以將原來以回調(diào)函數(shù)形式調(diào)用的代碼改為鏈式調(diào)用。
// jQuery ajax promise 方式
$.get('test.html')
.then(data => $(data))
.then($data => $data.find('#link').val('href'))
.then(href => console.log(href))
resolve 和 reject
自己定義一個 Promise 形式的函數(shù)在 ES6 當(dāng)中也非常簡單:
var promise = new Promise(function(resolve, reject) {
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(value) {
// failure
});
Promise 構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是 resolve 方法和 reject 方法。
如果異步操作成功,則用 resolve 方法將 Promise 對象的狀態(tài),從「未完成」變?yōu)椤赋晒Α梗磸?pending 變?yōu)?resolved);
如果異步操作失敗,則用 reject 方法將 Promise 對象的狀態(tài),從「未完成」變?yōu)椤甘 梗磸?pending 變?yōu)?rejected)。
then 和 catch
在多個任務(wù)的情況下 <code>then</code> 方法可以用一個清晰的方式完成。<code>then</code>可以使用鏈式調(diào)用的寫法原因在于,每一次執(zhí)行該方法時總是會返回一個 Promise 對象。另外,在<code>then</code> onFulfilled的函數(shù)當(dāng)中的返回值,可以作為后續(xù)操作的參數(shù)。
function printHello (ready) {
return new Promise(function (resolve, reject) {
if (ready) {
resolve("Hello");
} else {
reject("Good bye!");
}
});
}
printHello(true).then(function (message) {
return message;
}).then(function (message) {
return message + ' World';
}).then(function (message) {
return message + '!';
}).then(function (message) {
alert(message);
}).catch(function(reason){ // error
console.log(reason);
});
Promise.all 和 Promise.race
<code>Promise.all</code> 可以接收一個元素為<code>Promise</code> 對象的數(shù)組作為參數(shù),當(dāng)這個數(shù)組里面所有的 Promise 對象都變?yōu)?resolve 時,該方法才會返回。
var p1 = new Promise(function (resolve) {
setTimeout(function () {
resolve("Hello");
}, 3000);
});
var p2 = new Promise(function (resolve) {
setTimeout(function () {
resolve("World");
}, 1000);
});
Promise.all([p1, p2]).then(function (result) {
console.log(result); // ["Hello", "World"]
});
還有一個和 <code>Promise.all</code> 相類似的方法 <code>Promise.race</code> ,它同樣接收一個數(shù)組,不同的是只要該數(shù)組中的 Promise 對象的狀態(tài)發(fā)生變化(無論是 resolve 還是 reject)該方法都會返回。
兼容性
關(guān)于 Promise 對象的兼容性問題。

在瀏覽器端,一些主流的瀏覽器都已經(jīng)可以使用 Promise 對象進行開發(fā),在 Node.js 配合 babel 也可以很方便地使用。
如果要兼容舊的瀏覽器,建議可以尋找一些第三方的解決方案,例如 jQuery 的 <code>$.Deferred</code>。
util.promisify
在 node 8.0 以上的版本還可以利用 util.promisify 方法將回調(diào)形式的函數(shù)變?yōu)?Promise 形式。
const util = require('util')
const fs = require('fs')
const readPromise = util.promisify(fs.readFile)
readPromise('test.txt').then(data => console.log(data.toString()))
三、Generators
什么是生成器?
我們從一個示例開始:
function* quips(name) {
yield "你好 " + name + "!";
yield "希望你能喜歡這篇介紹ES6的譯文";
if (name.startsWith("X")) {
yield "你的名字 " + name + " 首字母是X,這很酷!";
}
yield "我們下次再見!";
}
這段代碼看起來很像一個函數(shù),我們稱之為生成器函數(shù),它與普通函數(shù)有很多共同點,但是二者有如下區(qū)別:
- 普通函數(shù)使用function聲明,而生成器函數(shù)使用function*聲明。
- 在生成器函數(shù)內(nèi)部,有一種類似return的語法:關(guān)鍵字yield。二者的區(qū)別是,普通函數(shù)只可以return一次,而生成器函數(shù)可以yield多次(當(dāng)然也可以只yield一次)。在生成器的執(zhí)行過程中,遇到y(tǒng)ield表達式立即暫停,后續(xù)可恢復(fù)執(zhí)行狀態(tài)。
這就是普通函數(shù)和生成器函數(shù)之間最大的區(qū)別,普通函數(shù)不能自暫停,生成器函數(shù)可以。
生成器做了什么?
當(dāng)你調(diào)用quips()生成器函數(shù)時發(fā)生了什么?
> var iter = quips("jorendorff");
[object Generator]
> iter.next()
{ value: "你好 jorendorff!", done: false }
> iter.next()
{ value: "希望你能喜歡這篇介紹ES6的譯文", done: false }
> iter.next()
{ value: "我們下次再見!", done: false }
> iter.next()
{ value: undefined, done: true }
生成器是迭代器
我們學(xué)習(xí)了ES6的迭代器,它是ES6中獨立的內(nèi)建類,同時也是語言的一個擴展點,通過實現(xiàn)<code>Symbol.iterator()</code>和<code>.next()</code>兩個方法你就可以創(chuàng)建自定義迭代器。
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
} else {
return {done: true, value: undefined};
}
}
}
// 返回一個新的迭代器,可以從start到stop計數(shù)。
function range(start, stop) {
return new RangeIterator(start, stop);
}
四、Async/Await
node7.6 以上的版本引入了一個 ES7 的新特性 Async/Await 是專門用于控制異步代碼。先看一個例子:
const util = require('util')
const fs = require('fs')
const readFile = util.promisify(fs.readFile)
async function readFiles () {
const txt = await readFile('file1.txt', 'utf8')
console.log(txt)
const txt2 = await readFile('file2.txt', 'utf8')
console.log(txt2)
})
首先要使用<code>async</code> 關(guān)鍵字定義一個包含異步代碼的函數(shù),在 Promise 形式的異步函數(shù)前面使用 <code>await</code> 關(guān)鍵字就可以將異步寫成同步操作的形式。
看上去與 Generators 控制方式相差不大,但是 Async/Await 是原生用于控制異步,所以是比較推薦使用的。
五、錯誤處理
最后來探討下四種異步控制方法的錯誤處理。
回調(diào)函數(shù)
回調(diào)函數(shù)錯誤處理非常簡單,就是在回調(diào)函數(shù)中同時回傳錯誤信息:
const fs = require('fs')
fs.readFile('file.txt', (err, data) => {
if (err) {
throw err
}
console.log(data)
})
Promise
Promise 在 then 方法之后使用一個 catch 方案來捕捉錯誤信息:
const fs = require('fs')
const readFile = util.promisify(fs.readFile)
readFile('file.txt')
.then(data => console.log(data))
.catch(err => console.log(err))
Generators 和 Async/Await
Generators 和 Async/Await 比較類似,可以有兩種方式,第一種使用 Promise 的 catch 方法,第二種用 try catch 關(guān)鍵字。
Promise catch
const fs = require('fs')
const co = require('co')
const readFile = util.promisify(fs.readFile)
co(function* () {
const data = yield readFile('file.txt').catch(err => console.log(err))
})
const fs = require('fs')
const co = require('co')
const readFile = util.promisify(fs.readFile)
async function testRead() {
const data = await readFile('file.txt').catch(err => console.log(err))
}
try/catch
const fs = require('fs')
const co = require('co')
const readFile = util.promisify(fs.readFile)
co(function* () {
try {
const data = yield readFile('file.txt')
} catch(err) {
console.log(err)
}
})
const fs = require('fs')
const readFile = util.promisify(fs.readFile)
async function testRead() {
try {
const data = await readFile('file.txt')
} catch(err) {
console.log(data)
}
}