深刻理解Javascript之Promise

目錄:

1.概述

相信你們都聽過Node中著名的回調地獄(callback hell)。由於Node中的操做默認都是異步執行的,因此須要調用者傳入一個回調函數以便在操做結束時進行相應的處理。當回調的層次變多,代碼就變得愈來愈難以編寫、理解和閱讀。javascript

Promise是ES6中新增的一種異步編程的方式,用於解決回調的方式的各類問題,提供了更多的可能性。其實早在ES6以前,社區就已經有多種Promise的實現方式了:java

以上幾種Promise庫都遵循Promise/A+規範。ES6也採用了該規範,因此這些實現的API都是相似的,能夠相互對照學習。git

Promise表示的是一個計算結果或網絡請求的佔位符。因爲當前計算或網絡請求還沒有完成,因此結果暫時沒法取得。es6

Promise對象一共有3中狀態,pendingfullfilled(又稱爲resolved)和rejectedgithub

  • pending——任務仍在進行中。
  • resolved——任務已完成。
  • reject——任務出錯。

Promise對象初始時處於pending狀態,其生命週期內只可能發生如下一種狀態轉換:web

  • 任務完成,狀態由pending轉換爲resolved
  • 任務出錯返回,狀態由pending轉換爲rejected

Promise對象的狀態轉換一旦發生,就不可再次更改。這或許就是Promise之「承諾」的含義吧。編程

2.基本用法

2.1 建立Promise

Javascript提供了Promise構造函數用於建立Promise對象。格式以下:json

let p = new Promise(executor(resolve, reject));
複製代碼

代碼中executor是用戶自定義的函數,用於實現具體異步操做流程。該函數有兩個參數resolvereject,它們是Javascript引擎提供的函數,不須要用戶實現。在executor函數中,若是異步操做成功,則調用resolvePromise的狀態轉換爲resolvedresolve函數以結果數據做爲參數。若是異步操做失敗,則調用rejectPromise的狀態轉換爲rejectedreject函數以具體錯誤對象做爲參數。api

2.2 then方法

Promise對象建立完成以後,咱們須要調用then(succ_handler, fail_handler)方法指定成功和/或失敗的回調處理。例如:數組

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);
});
複製代碼

在上面的代碼中,咱們建立了一個Promise對象,在executor函數中調用resolve將該對象狀態轉換爲resolved

進而then指定的成功回調函數被調用,輸出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函數中調用rejectPromise對象狀態轉換爲rejected

進而then指定的失敗回調函數被調用,輸出oh no, something be wrong

這就是最基本的使用Promise編寫異步處理的方式了。可是,有幾點須要注意:

(1) then方法能夠只傳入成功或失敗回調。

(2)executor函數是當即執行的,而成功或失敗的回調函數會到當前EventLoop的最後再執行。下面的代碼能夠驗證這一點:

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

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

console.log("end");
複製代碼

輸出結果爲:

promise constructor
end
finished
複製代碼

(3) then方法返回的是一個新的Promise對象,因此能夠鏈式調用:

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

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

(4)Promise對象的then方法能夠被調用屢次,並且能夠被重複調用(不一樣於事件,同一個事件的回調只會被調用一次。)。

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方法指定錯誤處理。可是ES6提供了一個更好用的方法catch。直觀上理解能夠認爲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方法中指定錯誤處理,而是在調用鏈的最後增長一個catch方法用於處理前面的步驟中出現的錯誤。

使用時注意一下幾點:

  • then方法指定兩個處理函數,調用成功處理函數拋出異常時,失敗處理函數不會被調用

  • Promise中未被處理的異常不會終止當前的執行流程,也就是說Promise會**「吞掉異常」**。

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

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

console.log("end");
// 程序正常結束,輸出end
複製代碼

2.4 其餘建立Promise對象的方式

除了Promise構造函數,ES6還提供了兩個簡單易用的建立Promise對象的方式,即Promise.resolvePromise.reject

Promise.resolve

顧名思義,Promise.resolve建立一個resolved狀態的Promise對象:

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

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

Promise.resolve的參數分爲如下幾種類型:

(1)參數是一個Promise對象,那麼直接返回該對象。

(2) 參數是一個thenable對象,即擁有then函數的對象。這時Promise.resolve會將該對象轉換爲一個Promise對象,而且當即執行其then函數。

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

let p = Promise.resolve(thenable);

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

(3)其餘參數(無參數至關於有一個undefined參數),建立一個狀態爲resolvedPromise對象,參數做爲操做結果會傳遞給後續回調處理。

Promise.reject

Promise.reject無論參數爲什麼種類型,都是建立一個狀態爲rejectedPromise對象。

3.高級用法

3.1 Flatten Promise

then方法的成功回調函數能夠返回一個新的Promise對象,這時舊的Promise對象將會被凍結,其狀態取決於新Promise對象的狀態。

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
});
複製代碼

咱們在(A)行直接返回了另外一個Promise對象。後面的then方法執行取決於該對象的狀態,因此在3s後輸出promise1,不會輸出promise2

3.2 Promise.all 方法

不少時候,咱們想要等待多個異步操做完成後再進行一些處理。若是使用回調的方式,會出現前面提到過的回調地獄。例如:

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);
        });
    });
});
複製代碼

假設文件file1file2file3中的內容分別是"in file1","in file2","in file3"。那麼輸出以下:

in file1
in file2
in file3
複製代碼

這種狀況下,Promise.all就派上大用場了。Promise.all接受一個可迭代對象(即ES6中的Iterable對象),每一個元素經過調用Promise.resolve轉換爲Promise對象。Promise.all方法返回一個新的Promise對象。該對象在全部Promise對象狀態變爲resolved時,其狀態纔會轉換爲resolved,參數爲各個Promise的結果組成的數組。只要有一個對象的狀態變爲rejected,新對象的狀態就會轉換爲rejected。使用Promise.all咱們能夠很優雅的實現上面的功能:

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']
複製代碼

第二段代碼咱們能夠進一步簡化爲:

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同樣,接受一個可迭代對象做爲參數,返回一個新的Promise對象。不一樣的是,只要參數中有一個Promise對象狀態發生變化,新對象的狀態就會變化。也就是說哪一個操做快,就用哪一個結果(或出錯)。利用這種特性,咱們能夠實現超時處理:

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

let p2 = new Promise(function (resolve, reject) {
    // 模擬耗時操做
    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);
});
複製代碼

對象p1在1s以後狀態轉換爲rejectedp2在2s後轉換爲resolved。因此1s後,p1狀態轉換時,p的狀態緊接着就轉爲rejected了。從而,輸出爲:

time out
複製代碼

若是將對象p2的延遲改成0.5s,那麼在0.5s後p2狀態改變時,p緊隨其後狀態轉換爲resolved。從而輸出爲:

get result
複製代碼

4.使用案例

前面咱們提到過,then方法會返回一個新的Promise對象。因此then方法能夠鏈式調用,前一個成功回調的返回值會做爲下一個成功回調的參數。例如:

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);
});
複製代碼

對象p狀態變爲resolved時,結果爲25。行(A)處函數最早被調用,參數num的值爲25,返回值爲2626又做爲行(B)處函數的參數,函數返回5252做爲行(C)處函數的參數,被輸出。

下面給出結合AJAX的一個案例。

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函數接受一個url地址,請求json數據。可是請求到的數據是文本格式,因此在第一個then方法的回調中使用JSON.parse將其轉爲對象,第二個then方法回調再進行具體處理。

http://api.icndb.com/jokes/random是一個隨機笑話的api,你們能夠試試 :smile:。

5.總結

Promise是ES6新增的一種異步編程的解決方案,使用它能夠編寫更優雅,更易讀,更易維護的程序。Promise已經應用在各個角落了,我的認爲掌握它是一個合格的Javascript開發者的基本功。

6.參考連接

JavaScript Promise:簡介

Tasks, microtasks, queues and schedules

How to escape Promise Hell

An Overview of JavaScript Promise

ES6 Promise:Promise語法介紹

Promise 對象:阮一峯老師Promise對象詳解

關於我: 我的主頁 簡書 掘金

相關文章
相關標籤/搜索