es6 異步編程之 Promise 從認(rèn)識(shí)到使用

說起 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 是什么?

Promise1.png

在谷歌瀏覽器的控制臺(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ì))
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Promiese 簡(jiǎn)單說就是一個(gè)容器,里面保存著某個(gè)未來才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果,語法上說,Pr...
    雨飛飛雨閱讀 3,489評(píng)論 0 19
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 11,130評(píng)論 26 95
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 8,776評(píng)論 0 29
  • 你不知道JS:異步 第三章:Promises 在第二章,我們指出了采用回調(diào)來表達(dá)異步和管理并發(fā)時(shí)的兩種主要不足:缺...
    purple_force閱讀 2,243評(píng)論 0 4
  • 異步編程對(duì)JavaScript語言太重要。Javascript語言的執(zhí)行環(huán)境是“單線程”的,如果沒有異步編程,根本...
    呼呼哥閱讀 7,404評(píng)論 5 22

友情鏈接更多精彩內(nèi)容