說起 ES6 異步編程,你可能會(huì)想到 Generator、async、Promise 以及各種第三方庫,如:co 等。認(rèn)識(shí)這幾個(gè)名詞是源于在網(wǎng)上看到一些帖子在辯論哪種異步編程更完美,當(dāng)時(shí)的我對(duì)異步編程的概念還不是很熟悉,更無須談哪種更好以及在我的代碼中使用它們了。本文總結(jié)異步編程的概念以及什么時(shí)候會(huì)用到這些技術(shù)。
一、傳統(tǒng)異步解決方案
在 Promise 等 es6 異步編程出現(xiàn)之前,傳統(tǒng)異步是通過回調(diào)函數(shù)進(jìn)行處理的。想一想 ajax 的 success 函數(shù)就是處理 ajax 異步成功的一個(gè)回調(diào)函數(shù)。
1. 什么是異步
傳統(tǒng)編程都是順序執(zhí)行代碼的,下面代碼正常輸出順序應(yīng)該是:step1 > step2 > step3。
// asyncfunc.js
function step2 () {
setTimeout (() => {
console.log('step2');
}, 3000);
}
console.log('step1');
step2();
console.log('step3');
在控制臺(tái)打印結(jié)果:先打印 step1 和 step3,過幾秒之后才會(huì)打印出 step2,這無疑與傳統(tǒng)編程的順序執(zhí)行并不同。這種變更代碼執(zhí)行順序的行為就叫做異步。
D:\code\es6\promise-demo>node asyncfunc.js
step1
step3
step2
常見的異步操作有 ajax,這里的 setTimeout 是模擬異步操作的一種方式。
2. 回調(diào)函數(shù)完成異步操作
在出現(xiàn) es6 異步編程之前,傳統(tǒng)的異步是通過回調(diào)函數(shù)完成的。下面實(shí)現(xiàn)一個(gè)泡茶操作:先燒水(水燒開需要5秒),再進(jìn)行泡茶。
// callbackForAsync.js
// v1.0
// 燒水
function boilWater () {
setTimeout(() => {
console.log('水剛剛燒開,可以泡茶了');
}, 5000);
}
// 泡茶
function makeTea () {
console.log('水已經(jīng)燒開了,開始泡茶');
}
boilWater(); // 燒水
makeTea(); // 泡茶
這里寫了兩個(gè)方法,一個(gè)燒水,一個(gè)泡茶,燒水方法里用了一個(gè)延遲函數(shù) setTimeout,因?yàn)闊@個(gè)過程需要 5 s 時(shí)間。之后調(diào)用這兩個(gè)方法,先調(diào)用燒水,再調(diào)用泡茶。
打印結(jié)果:
D:\code\es6\promise-demo>node callbackForAsync.js
水已經(jīng)燒開了,開始泡茶
水剛剛燒開,可以泡茶了
盡管我們先調(diào)用了燒水方法,再調(diào)用泡茶方法,但打印的結(jié)果與我們期待的并不同。這是因?yàn)檫@個(gè)過程用到了異步的思想。燒水是個(gè)異步過程,但是這里的寫法是順序執(zhí)行的,泡茶并不會(huì)等待燒水完成再執(zhí)行,而我們期望的就是泡茶等待燒水完成。
// callbackForAsync.js
// v2.0
// 燒水
function boilWater (callback) {
setTimeout(() => {
console.log('水剛剛燒開,可以泡茶了');
callback();
}, 5000);
}
// 泡茶
function makeTea () {
console.log('水已經(jīng)燒開了,開始泡茶');
}
boilWater(makeTea); // 燒水 + 泡茶
對(duì) v1.0 代碼進(jìn)行修改,將泡茶方法作為參數(shù)傳遞給燒水方法,在燒水方法內(nèi)部調(diào)用調(diào)用方法就可以實(shí)現(xiàn)我們的期待值。
D:\code\es6\promise-demo>node callbackForAsync.js
水剛剛燒開,可以泡茶了
水已經(jīng)燒開了,開始泡茶
3. 回調(diào)地獄
前面只有兩步操作,通過一個(gè)回調(diào)可以很容易的實(shí)現(xiàn),現(xiàn)在加一個(gè)操作,泡完茶之后進(jìn)行喝茶操作,如何實(shí)現(xiàn)??
// callbackHell.js
// 燒水
function boilWater (callback, callback2) {
setTimeout(() => {
console.log('水剛剛燒開,可以泡茶了');
callback(callback2);
}, 5000);
}
// 泡茶
function makeTea (callback2) {
console.log('水已經(jīng)燒開了,開始泡茶');
callback2();
}
// 喝茶
function drinkTea () {
console.log('茶泡好了,正在喝茶');
}
// 燒水 > 泡茶 > 喝茶
boilWater(makeTea, drinkTea);
上述代碼中,將喝茶操作作為參數(shù)先傳遞給燒水方法,在燒水方法內(nèi)部將喝茶方法作為參數(shù)傳遞給泡茶方法,最后在泡茶方法內(nèi)部再調(diào)用喝茶方法??梢娺@里只有兩步邏輯,喝茶方法在燒水方法和泡茶方法都有出現(xiàn),如果喝茶操作之后還有操作,那么類推會(huì)不停的進(jìn)行嵌套嵌套,這種實(shí)現(xiàn)不僅不美觀,而且還不方便后期代碼維護(hù)。
二、Promise 是什么?

在谷歌瀏覽器的控制臺(tái)(按 F12)中打印 console.dir(Promise),可以看到上圖。
- Promise 是一個(gè)構(gòu)造函數(shù)(只有構(gòu)造函數(shù)的函數(shù)名首字母才大寫,這是規(guī)范),本身擁有三個(gè)方法:all、race 、reject、resolve;
- 原型鏈對(duì)象擁有兩個(gè)方法:catch、then,原型鏈上的方法通過 new 實(shí)例對(duì)象才能調(diào)用。
三、Promise 基礎(chǔ)寫法
Demo1:創(chuàng)建 Promise 實(shí)例對(duì)象
// promiseBaseDemo.js
// v1.0
var myPromise = new Promise(function (resolve, reject) {
console.log('peomise 內(nèi)部代碼');
resolve('end');
});
Promise 是個(gè)構(gòu)造函數(shù),通過 new 得到實(shí)例對(duì)象 myPromise;構(gòu)造函數(shù)的參數(shù)有兩個(gè) resolve 和 reject 兩個(gè)形參,這兩個(gè)參數(shù)就是在控制臺(tái)中輸入 console.dir(Promise) 打印出來的那兩個(gè)屬于構(gòu)造函數(shù)的方法,有什么用,接下來說。
打印結(jié)果:
D:\code\es6\promise-demo>node promiseBaseDemo.js
peomise 內(nèi)部代碼
結(jié)果打印出了實(shí)例對(duì)象內(nèi)部的代碼。通常來說使用 new 創(chuàng)建的實(shí)例對(duì)象并不會(huì)打印任何信息,只有調(diào)用這個(gè)方法,如:myPromise() 才會(huì)執(zhí)行代碼,但是這里卻打印了東東。
** 特性:Promise 構(gòu)造出的實(shí)例對(duì)象會(huì)自執(zhí)行。 **
Demo2:構(gòu)造函數(shù)的 resolve 方法和實(shí)例對(duì)象的 then 方法
// promiseBaseDemo.js
// v2.0
var myPromise = new Promise(function (resolve, reject) {
console.log('peomise 內(nèi)部代碼');
resolve('end');
});
myPromise.then(function (data) {
console.log(data);
});
myPromise 是 Promise 的實(shí)例對(duì)象,擁有原型鏈方法 then。
打印結(jié)果:
D:\code\es6\promise-demo>node promiseBaseDemo.js
peomise 內(nèi)部代碼
end
then 方法為 Promise 的原型鏈方法,接收一個(gè)函數(shù)作為參數(shù),如上述代碼,data 表示 new 實(shí)例對(duì)象時(shí) resolve() 里面的內(nèi)容。
Demo3:構(gòu)造函數(shù)的 reject 方法和原型鏈對(duì)象的 catch 方法
// promiseBaseDemo.js
// v3.0
var myPromise = new Promise(function (resolve, reject) {
console.log('peomise 內(nèi)部代碼');
if (0 > 1) {
resolve('end');
} else {
reject('出錯(cuò)了');
}
});
myPromise.then(function (data) {
console.log(data);
}).catch(function (error) {
console.log(error);
});
resolve 返回成功的數(shù)據(jù),reject 返回失敗的數(shù)據(jù)。樓主剛開始學(xué)習(xí)這里不是很理解,這里寫貼上代碼看下打印結(jié)果,下面通過實(shí)例感受區(qū)別。
catch 方法用來處理異常,也就是處理 reject 方法,保持程序不會(huì)直接掛掉,仍然可以繼續(xù)執(zhí)行。同樣的 then 方法就是處理 resolve 方法。
打印結(jié)果:
D:\code\es6\promise-demo>node promiseBaseDemo.js
peomise 內(nèi)部代碼
出錯(cuò)了
Demo4:規(guī)避 Promise 實(shí)例對(duì)象自執(zhí)行
前面說到了 Promise 創(chuàng)建實(shí)例對(duì)象會(huì)自執(zhí)行,這顯示不是我們想要的,作為控制欲強(qiáng)盛的程序員,要做到我想讓你執(zhí)行你才能執(zhí)行,不想讓你執(zhí)行就不能執(zhí)行。
// promiseBaseDemo.js
// v4.0
function myPromise () {
return new Promise(function (resolve, reject) {
console.log('peomise 內(nèi)部代碼');
if (0 > 1) {
resolve('end');
} else {
reject('出錯(cuò)了');
}
});
}
// 想要執(zhí)行解除下面代碼的注釋
// myPromise().then(function (data) {
// console.log(data);
// }).catch(function (error) {
// console.log(error);
// });
將 new Promise 這個(gè)過程封裝到一個(gè)函數(shù)中,并且在函數(shù)內(nèi)部返回 Promise 實(shí)例對(duì)象。
注意:執(zhí)行方法變成了 myPromise() 而不是之前的 myPromise。
四、Promise 同步控制多個(gè)異步操作的執(zhí)行順序
1. 多個(gè)異步操作
// multiAsync.js
var boilWater = function () {
setTimeout(() => {
console.log('step1: 水剛剛燒開,可以泡茶了');
}, 5000);
}
var makeTea = function () {
setTimeout(() => {
console.log('step2: 水已經(jīng)燒開了,開始泡茶');
}, 2000);
}
// 喝茶:異步操作,需要 1 s
var drinkTea = function () {
setTimeout(() => {
console.log('step3: 茶泡好了,正在喝茶');
}, 1000);
}
boilWater();
makeTea();
drinkTea();
打印結(jié)果:
D:\code\es6\promise-demo>node multiAsync.js
step3: 茶泡好了,正在喝茶
step2: 水已經(jīng)燒開了,開始泡茶
step1: 水剛剛燒開,可以泡茶了
像上面的代碼,多個(gè)異步操作,按照正常寫法,我們無法控制多個(gè)異步操作的執(zhí)行順序。Promise 可以控制多個(gè)異步操作的順序,并且告別回調(diào)地獄,按照同步的寫法去書寫。
2. Promise 同步控制多個(gè)異步操作的順序
// promiseForAsync.js
// 燒水:異步操作,需要 5 s
var boilWater = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step1: 水剛剛燒開,可以泡茶了');
}, 5000);
});
}
// 泡茶:異步操作,需要 2 s
var makeTea = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step2: 水已經(jīng)燒開了,開始泡茶');
}, 2000);
});
}
// 喝茶:異步操作,需要 1 s
var drinkTea = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step3: 茶泡好了,正在喝茶');
}, 1000);
});
}
var arr = []; // 創(chuàng)建數(shù)組,記錄三個(gè)異步操作執(zhí)行順序
console.time('promise');
boilWater().then((data) => {
arr.push(data);
// makeTea() 返回的是 Promise 的實(shí)例對(duì)象,依次可以繼續(xù)使用 then 方法。下同。
return makeTea();
}).then((data) => {
arr.push(data);
return drinkTea();
}).then((data) => {
arr.push(data);
console.log(arr);
console.timeEnd('promise');
});
打印結(jié)果:
D:\code\es6\promise-demo>node promiseForAsync.js
[ 'step1: 水剛剛燒開,可以泡茶了',
'step2: 水已經(jīng)燒開了,開始泡茶',
'step3: 茶泡好了,正在喝茶' ]
promise: 8020.676ms
五、Promise.all 異步控制多個(gè)異步操作的執(zhí)行順序
在 promiseForAsync.js 中已經(jīng)控制了多個(gè)異步操作的順序,但這還不是我們想要的,異步順序確實(shí)控制住了,但執(zhí)行時(shí)間卻變成了三個(gè)異步操作分別執(zhí)行時(shí)間的和。
我們期待的結(jié)果,執(zhí)行時(shí)間依然異步(不能超過三個(gè)異步操作中時(shí)間最長(zhǎng)的那個(gè)時(shí)間,因?yàn)槿齻€(gè)異步操作是同時(shí)進(jìn)行的),執(zhí)行順序得到控制。
// promiseForAll.js
// 燒水:異步操作,需要 5 s
var boilWater = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step1: 水剛剛燒開,可以泡茶了');
}, 5000);
});
}
// 泡茶:異步操作,需要 2 s
var makeTea = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step2: 水已經(jīng)燒開了,開始泡茶');
}, 2000);
});
}
// 喝茶:異步操作,需要 1 s
var drinkTea = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step3: 茶泡好了,正在喝茶');
}, 1000);
});
}
console.time('promise');
Promise.all([boilWater(), makeTea(), drinkTea()]).then((result) => {
console.log(result);
console.timeEnd('promise');
});
打印結(jié)果:
D:\code\es6\promise-demo>node promiseForAll.js
[ 'step1: 水剛剛燒開,可以泡茶了',
'step2: 水已經(jīng)燒開了,開始泡茶',
'step3: 茶泡好了,正在喝茶' ]
promise: 5015.434ms
可以看到,這種寫法執(zhí)行順序依然得到控制,而執(zhí)行時(shí)間從 8s 變成了 5s,為什么這里不是剛好 5s,而是有零頭的時(shí)間,這是由于使用了 setTimeout 模擬異步,這個(gè)方法在執(zhí)行代碼的過程中會(huì)浪費(fèi)些許時(shí)間導(dǎo)致的。
六、總結(jié)
Promise 用來處理以下問題:
- 同時(shí)操作多個(gè)異步操作;
- 需要控制多個(gè)異步操作按照一定順序依次執(zhí)行;
- 有同步(promiseForAsync.js)和異步(promiseForAll.js)兩種寫法。(這里的同步和異步可以通過執(zhí)行時(shí)間 8s 和 5s 細(xì)細(xì)體會(huì))