深入理解Javascript之Promise

目錄:

1.概述

相信大家都聽過Node中著名的回調(diào)地獄(callback hell)。因?yàn)镹ode中的操作默認(rèn)都是異步執(zhí)行的,所以需要調(diào)用者傳入一個(gè)回調(diào)函數(shù)以便在操作結(jié)束時(shí)進(jìn)行相應(yīng)的處理。當(dāng)回調(diào)的層次變多,代碼就變得越來越難以編寫、理解和閱讀。

Promise是ES6中新增的一種異步編程的方式,用于解決回調(diào)的方式的各種問題,提供了更多的可能性。其實(shí)早在ES6之前,社區(qū)就已經(jīng)有多種Promise的實(shí)現(xiàn)方式了:

以上幾種Promise庫(kù)都遵循Promise/A+規(guī)范。ES6也采用了該規(guī)范,所以這些實(shí)現(xiàn)的API都是類似的,可以相互對(duì)照學(xué)習(xí)。

Promise表示的是一個(gè)計(jì)算結(jié)果或網(wǎng)絡(luò)請(qǐng)求的占位符。由于當(dāng)前計(jì)算或網(wǎng)絡(luò)請(qǐng)求尚未完成,所以結(jié)果暫時(shí)無法取得。

Promise對(duì)象一共有3中狀態(tài),pending,fullfilled(又稱為resolved)和rejected

  • pending——任務(wù)仍在進(jìn)行中。
  • resolved——任務(wù)已完成。
  • reject——任務(wù)出錯(cuò)。

Promise對(duì)象初始時(shí)處于pending狀態(tài),其生命周期內(nèi)只可能發(fā)生以下一種狀態(tài)轉(zhuǎn)換:

  • 任務(wù)完成,狀態(tài)由pending轉(zhuǎn)換為resolved。
  • 任務(wù)出錯(cuò)返回,狀態(tài)由pending轉(zhuǎn)換為rejected。

Promise對(duì)象的狀態(tài)轉(zhuǎn)換一旦發(fā)生,就不可再次更改。這或許就是Promise之“承諾”的含義吧。

2.基本用法

2.1 創(chuàng)建Promise

Javascript提供了Promise構(gòu)造函數(shù)用于創(chuàng)建Promise對(duì)象。格式如下:

let p = new Promise(executor(resolve, reject));

代碼中executor是用戶自定義的函數(shù),用于實(shí)現(xiàn)具體異步操作流程。該函數(shù)有兩個(gè)參數(shù)resolvereject,它們是Javascript引擎提供的函數(shù),不需要用戶實(shí)現(xiàn)。在executor函數(shù)中,如果異步操作成功,則調(diào)用resolvePromise的狀態(tài)轉(zhuǎn)換為resolved,resolve函數(shù)以結(jié)果數(shù)據(jù)作為參數(shù)。如果異步操作失敗,則調(diào)用rejectPromise的狀態(tài)轉(zhuǎn)換為rejected,reject函數(shù)以具體錯(cuò)誤對(duì)象作為參數(shù)。

2.2 then方法

Promise對(duì)象創(chuàng)建完成之后,我們需要調(diào)用then(succ_handler, fail_handler)方法指定成功和/或失敗的回調(diào)處理。例如:

let p = new Promise(function(resolve, reject) {
    resolve("finished");
});

p.then(function (data) {
    console.log(data); // 輸出finished
}, function (err) {
    console.log("oh no, ", err.message);
});

在上面的代碼中,我們創(chuàng)建了一個(gè)Promise對(duì)象,在executor函數(shù)中調(diào)用resolve將該對(duì)象狀態(tài)轉(zhuǎn)換為resolved。

進(jìn)而then指定的成功回調(diào)函數(shù)被調(diào)用,輸出finished。

let p = new Promise(function(resolve, reject) {
    reject(new Error("something be wrong"));
});

p.then(function (data) {
    console.log(data);
}, function (err) {
    console.log("oh no, ", err); // 輸出oh no,  something be wrong
});

以上代碼中,在executor函數(shù)中調(diào)用rejectPromise對(duì)象狀態(tài)轉(zhuǎn)換為rejected

進(jìn)而then指定的失敗回調(diào)函數(shù)被調(diào)用,輸出oh no, something be wrong。

這就是最基本的使用Promise編寫異步處理的方式了。但是,有幾點(diǎn)需要注意:

(1) then方法可以只傳入成功或失敗回調(diào)。

(2)executor函數(shù)是立即執(zhí)行的,而成功或失敗的回調(diào)函數(shù)會(huì)到當(dāng)前EventLoop的最后再執(zhí)行。下面的代碼可以驗(yàn)證這一點(diǎn):

let p = new Promise(function(resolve, reject) {
    console.log("promise constructor");
    resolve("finished");
});

p.then(function (data) {
    console.log(data);
});

console.log("end");

輸出結(jié)果為:

promise constructor
end
finished

(3) then方法返回的是一個(gè)新的Promise對(duì)象,所以可以鏈?zhǔn)秸{(diào)用:

let p = new Promise(function(resolve) {
    resolve(5);
});

p.then(function (data) {
    return data * 2;
})
 .then(function (data) {
    console.log(data); // 輸出10
});

(4)Promise對(duì)象的then方法可以被調(diào)用多次,而且可以被重復(fù)調(diào)用(不同于事件,同一個(gè)事件的回調(diào)只會(huì)被調(diào)用一次。)。

let p = new Promise(function(resolve) {
    resolve("repeat");
});

p.then(function (data) {
    console.log(data);
});

p.then(function (data) {
    console.log(data);
});

p.then(function (data) {
    console.log(data);
});

輸出:

repeat
repeat
repeat

2.3 catch方法

由前面的介紹,我們知道,可以由then方法指定錯(cuò)誤處理。但是ES6提供了一個(gè)更好用的方法catch。直觀上理解可以認(rèn)為catch(handler)等同于then(null, handler)。

let p = new Promise(function(resolve, reject) {
    reject(new Error("something be wrong"));
});

p.catch(function (err) {
    console.log("oh no, ", err.message); // 輸出oh no, something be wrong
});

通常不建議在then方法中指定錯(cuò)誤處理,而是在調(diào)用鏈的最后增加一個(gè)catch方法用于處理前面的步驟中出現(xiàn)的錯(cuò)誤。

使用時(shí)注意一下幾點(diǎn):

  • then方法指定兩個(gè)處理函數(shù),調(diào)用成功處理函數(shù)拋出異常時(shí),失敗處理函數(shù)不會(huì)被調(diào)用

  • Promise中未被處理的異常不會(huì)終止當(dāng)前的執(zhí)行流程,也就是說Promise會(huì)“吞掉異?!?/strong>。

let p = new Promise(function (resolve, reject) {
    throw new Error("something be wrong");
});

p.then(function (data) {
    console.log(data);
});

console.log("end");
// 程序正常結(jié)束,輸出end

2.4 其他創(chuàng)建Promise對(duì)象的方式

除了Promise構(gòu)造函數(shù),ES6還提供了兩個(gè)簡(jiǎn)單易用的創(chuàng)建Promise對(duì)象的方式,即Promise.resolvePromise.reject

Promise.resolve

顧名思義,Promise.resolve創(chuàng)建一個(gè)resolved狀態(tài)的Promise對(duì)象:

let p = Promise.resolve("hello");

p.then(function (data) {
    console.log(data); // 輸出hello
});

Promise.resolve的參數(shù)分為以下幾種類型:

(1)參數(shù)是一個(gè)Promise對(duì)象,那么直接返回該對(duì)象。

(2) 參數(shù)是一個(gè)thenable對(duì)象,即擁有then函數(shù)的對(duì)象。這時(shí)Promise.resolve會(huì)將該對(duì)象轉(zhuǎn)換為一個(gè)Promise對(duì)象,并且立即執(zhí)行其then函數(shù)。

let thenable = {
    then: function (resolve, reject) {
        resolve(25);
    };
};

let p = Promise.resolve(thenable);

p.then(function (data) {
    console.log(data); // 輸出25
});

(3)其他參數(shù)(無參數(shù)相當(dāng)于有一個(gè)undefined參數(shù)),創(chuàng)建一個(gè)狀態(tài)為resolvedPromise對(duì)象,參數(shù)作為操作結(jié)果會(huì)傳遞給后續(xù)回調(diào)處理。

Promise.reject

Promise.reject不管參數(shù)為何種類型,都是創(chuàng)建一個(gè)狀態(tài)為rejectedPromise對(duì)象。

3.高級(jí)用法

3.1 Flatten Promise

then方法的成功回調(diào)函數(shù)可以返回一個(gè)新的Promise對(duì)象,這時(shí)舊的Promise對(duì)象將會(huì)被凍結(jié),其狀態(tài)取決于新Promise對(duì)象的狀態(tài)。

let p1 = new Promise(function (resolve) {
    setTimeout(function () {
        resolve("promise1");
    }, 3000);
});

let p2 = new Promise(function (resolve) {
    resolve("promise2");
});

p2.then(function (data) {
    return p1;  // (A)
})
  .then(function (data) { // (B)
    console.log(data); // 輸出promise2
});

我們?cè)?A)行直接返回了另一個(gè)Promise對(duì)象。后面的then方法執(zhí)行取決于該對(duì)象的狀態(tài),所以在3s后輸出promise1,不會(huì)輸出promise2。

3.2 Promise.all 方法

很多時(shí)候,我們想要等待多個(gè)異步操作完成后再進(jìn)行一些處理。如果使用回調(diào)的方式,會(huì)出現(xiàn)前面提到過的回調(diào)地獄。例如:

let fs = require("fs");

fs.readFile("file1", "utf8", function (data1, err1) {
    if (err1 != nil) {
        console.log(err1);
        return;
    }

    fs.readFile("file2", "utf8", function (data2, err2) {
        if (err2 != nil) {
            console.log(err2);
            return;
        }

        fs.readFile("file3", "utf8", function (data3, err3) {
            if (err3 != nil) {
                console.log(err3);
                return;
            }

            console.log(data1);
            console.log(data2);
            console.log(data3);
        });
    });
});

假設(shè)文件file1file2,file3中的內(nèi)容分別是"in file1","in file2","in file3"。那么輸出如下:

in file1
in file2
in file3

這種情況下,Promise.all就派上大用場(chǎng)了。Promise.all接受一個(gè)可迭代對(duì)象(即ES6中的Iterable對(duì)象),每個(gè)元素通過調(diào)用Promise.resolve轉(zhuǎn)換為Promise對(duì)象。Promise.all方法返回一個(gè)新的Promise對(duì)象。該對(duì)象在所有Promise對(duì)象狀態(tài)變?yōu)?code>resolved時(shí),其狀態(tài)才會(huì)轉(zhuǎn)換為resolved,參數(shù)為各個(gè)Promise的結(jié)果組成的數(shù)組。只要有一個(gè)對(duì)象的狀態(tài)變?yōu)?code>rejected,新對(duì)象的狀態(tài)就會(huì)轉(zhuǎn)換為rejected。使用Promise.all我們可以很優(yōu)雅的實(shí)現(xiàn)上面的功能:

let fs = require("fs");

let promise1 = new Promise(function (resolve, reject) {
    fs.readFile("file1", "utf8", function (err, data) {
        if (err != null) {
            reject(err);
        } else {
            resolve(data);
        }
    });
});

let promise2 = new Promise(function (resolve, reject) {
    fs.readFile("file2", "utf8", function (err, data) {
        if (err != null) {
            reject(err);
        } else {
            resolve(data);
        }
    });
});

let promise3 = new Promise(function (resolve, reject) {
    fs.readFile("file3", "utf8", function (err, data) {
        if (err != null) {
            reject(err);
        } else {
            resolve(data);
        }
    });
});

let p = Promise.all([promise1, promise2, promise3]);
p.then(function (datas) {
    console.log(datas);
})
 .catch(function (err) {
    console.log(err);
});

輸出如下:

['in file1', 'in file2', 'in file3']

第二段代碼我們可以進(jìn)一步簡(jiǎn)化為:

let fs = require("fs");

let myReadFile = function (filename) {
    return new Promise(function (resolve, reject) {
        fs.readFile(filename, "utf8", function (err, data) {
            if (err != null) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

let promise1 = myReadFile("file1");
let promise2 = myReadFile("file2");
let promise3 = myReadFile("file3");

let p = Promise.all([promise1, promise2, promise3]);
p.then(function (datas) {
    console.log(datas);
})
 .catch(function (err) {
    console.log(err);
});

3.3 Promise.race 方法

Promise.racePromise.all一樣,接受一個(gè)可迭代對(duì)象作為參數(shù),返回一個(gè)新的Promise對(duì)象。不同的是,只要參數(shù)中有一個(gè)Promise對(duì)象狀態(tài)發(fā)生變化,新對(duì)象的狀態(tài)就會(huì)變化。也就是說哪個(gè)操作快,就用哪個(gè)結(jié)果(或出錯(cuò))。利用這種特性,我們可以實(shí)現(xiàn)超時(shí)處理:

let p1 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        reject(new Error("time out"));
    }, 1000);
});

let p2 = new Promise(function (resolve, reject) {
    // 模擬耗時(shí)操作
    setTimeout(function () {
        resolve("get result");
    }, 2000);
});

let p = Promise.race([p1, p2]);

p.then(function (data) {
    console.log(data);
})
 .catch(function (err) {
    console.log(err);
});

對(duì)象p1在1s之后狀態(tài)轉(zhuǎn)換為rejected,p2在2s后轉(zhuǎn)換為resolved。所以1s后,p1狀態(tài)轉(zhuǎn)換時(shí),p的狀態(tài)緊接著就轉(zhuǎn)為rejected了。從而,輸出為:

time out

如果將對(duì)象p2的延遲改為0.5s,那么在0.5s后p2狀態(tài)改變時(shí),p緊隨其后狀態(tài)轉(zhuǎn)換為resolved。從而輸出為:

get result

4.使用案例

前面我們提到過,then方法會(huì)返回一個(gè)新的Promise對(duì)象。所以then方法可以鏈?zhǔn)秸{(diào)用,前一個(gè)成功回調(diào)的返回值會(huì)作為下一個(gè)成功回調(diào)的參數(shù)。例如:

let p = new Promise(function (resolve, reject) {
    resolve(25);
});

p.then(function (num) { // (A)
    return num + 1;
})
 .then(function (num) { // (B)
    return num * 2;
})
 .then(function (num) { // (C)
    console.log(num);
});

對(duì)象p狀態(tài)變?yōu)?code>resolved時(shí),結(jié)果為25。行(A)處函數(shù)最先被調(diào)用,參數(shù)num的值為25,返回值為26。26又作為行(B)處函數(shù)的參數(shù),函數(shù)返回5252作為行(C)處函數(shù)的參數(shù),被輸出。

下面給出結(jié)合AJAX的一個(gè)案例。

let getJSON = function (url) {
    return new Promise(function (resolve, reject) {
        let xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.onreadystatechange = function () {
            if (xhr.readyState !== 4) {
                return;
            }

            if (xhr.status === 200) {
                resolve(xhr.response);
            } else {
                reject(new Error(xhr.statusText));
            }
        }
        xhr.send();
    });
}

getJSON("http://api.icndb.com/jokes/random")
 .then(function (responseText) {
    return JSON.parse(responseText);
})
 .then(function (obj) {
    console.log(obj.value.joke);
})
 .catch(function (err) {
    console.log(err.message);
});

getJSON函數(shù)接受一個(gè)url地址,請(qǐng)求json數(shù)據(jù)。但是請(qǐng)求到的數(shù)據(jù)是文本格式,所以在第一個(gè)then方法的回調(diào)中使用JSON.parse將其轉(zhuǎn)為對(duì)象,第二個(gè)then方法回調(diào)再進(jìn)行具體處理。

http://api.icndb.com/jokes/random是一個(gè)隨機(jī)笑話的api,大家可以試試 :smile:。

5.總結(jié)

Promise是ES6新增的一種異步編程的解決方案,使用它可以編寫更優(yōu)雅,更易讀,更易維護(hù)的程序。Promise已經(jīng)應(yīng)用在各個(gè)角落了,個(gè)人認(rèn)為掌握它是一個(gè)合格的Javascript開發(fā)者的基本功。

6.參考鏈接

JavaScript Promise:簡(jiǎn)介

Tasks, microtasks, queues and schedules

How to escape Promise Hell

An Overview of JavaScript Promise

ES6 Promise:Promise語(yǔ)法介紹

Promise 對(duì)象:阮一峰老師Promise對(duì)象詳解

關(guān)于我:
個(gè)人主頁(yè) 簡(jiǎn)書 掘金

最后編輯于
?著作權(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)容

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