深入理解Javascript之Promise

2,038 阅读9分钟

目录:

1.概述

相信大家都听过Node中著名的回调地狱(callback hell)。因为Node中的操作默认都是异步执行的,所以需要调用者传入一个回调函数以便在操作结束时进行相应的处理。当回调的层次变多,代码就变得越来越难以编写、理解和阅读。

Promise是ES6中新增的一种异步编程的方式,用于解决回调的方式的各种问题,提供了更多的可能性。其实早在ES6之前,社区就已经有多种Promise的实现方式了:

以上几种Promise库都遵循Promise/A+规范。ES6也采用了该规范,所以这些实现的API都是类似的,可以相互对照学习。

Promise表示的是一个计算结果或网络请求的占位符。由于当前计算或网络请求尚未完成,所以结果暂时无法取得。

Promise对象一共有3中状态,pendingfullfilled(又称为resolved)和rejected

  • pending——任务仍在进行中。
  • resolved——任务已完成。
  • reject——任务出错。

Promise对象初始时处于pending状态,其生命周期内只可能发生以下一种状态转换:

  • 任务完成,状态由pending转换为resolved
  • 任务出错返回,状态由pending转换为rejected

Promise对象的状态转换一旦发生,就不可再次更改。这或许就是Promise之“承诺”的含义吧。

2.基本用法

2.1 创建Promise

Javascript提供了Promise构造函数用于创建Promise对象。格式如下:

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

代码中executor是用户自定义的函数,用于实现具体异步操作流程。该函数有两个参数resolvereject,它们是Javascript引擎提供的函数,不需要用户实现。在executor函数中,如果异步操作成功,则调用resolvePromise的状态转换为resolvedresolve函数以结果数据作为参数。如果异步操作失败,则调用rejectPromise的状态转换为rejectedreject函数以具体错误对象作为参数。

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对象详解

关于我: 个人主页 简书 掘金