ES6 异步编程之二:Promise

84563376 2019-06-21

异步回调的泥潭

异步回调是最直接的异步结果处理模式,将一个回调函数callback扔进异步处理函数中,当异步处理获得结果之后再调用这个回调函数就可以继续之后的处理,但是如果这个callback又是一个异步调用呢?众所周知的,在JavaScript中异步回调的层层嵌套几乎是反人性的,代码编写、修改和阅读对我等有代码洁癖的人而言是一种煎熬,这便是异步回调泥潭了。

今天对于处理异步调用已经有了很多成熟的方案,在我看来这些方案都无外乎在解决一个问题:“如何能看似顺序地传递异步调用的结果?”,本文要说的Promise就是ES6原生提供的一个解决方案。

在对Promise进行叙述之前,依旧引用阮大的《ECMAScript 6入门》一书中的Promise章节便于大家更严谨和全面的学习和参考。

Promise

承诺,即对未来的许诺,如果诺言实现,然后then)就如何如何……Promise极其生动的讲述了一个言出必行的故事。

new Promise(function(resolve, reject){
        //开始实现承诺
        ....
        ....
        if(承诺兑现时) {
           resolve(dollars);  //兑现承诺的结果是得到'一大笔美金'
        } else {
           reject('绝交');  //没兑现承诺就绝交
        }
    }).then(function(dollars){  //然后有钱了,买房买车娶妻生子
       let d1 = buyHouse(dollars); //把每次消费剩余的钱传给下一个函数
       let d2 = buyCar(d1);
       let d3 = marry(d2);
       makeBaby(d3);
    }).catch(function(result){//然后如果绝交了,还是继续吃土
       //继续吃土
    });
    console.log('故事开始....');

看过上面的这个俗不可耐的故事之后需要理解几件事情:

  1. 言出必行:一个Promise构造出来之后,构造时传入的异步函数就立即执行;*
    注:因大凡使用promise都是在异步调用场景,下文所说的异步函数都是指构造promise时传入的函数*

  2. Promise实例内部维护了一个状态机,状态变化只可能是pendingresolved或者pendingrejected;

    • 执行resolvepending变化到resolved

    • 执行rejectpending变化到rejected

    • 抛出错误:pending变化到rejected

  3. then的第一个回调函数只会在发生了resolve之后执行,本质上是在Promise到达resolved状态执行;

  4. then的第二个回调函数或者catch的回调函数会在发生reject之后或者异步函数执行抛出错误时执行,本质上是在Promise到达rejected状态时执行;

  5. 异步函数执行得到结果可以通过resolve或者reject将结果传出;

    • 调用resolve传入的值会作为then第一个回调函数的入参

    • 调用reject传入的值作为then第二个回调函数或者catch的回调函数的入参

    • 如果异步函数抛出了异常,异常会作为then第二个回调函数或者catch的回调函数的入参

  6. '故事开始....'会先输出,而不是等到then的回调函数执行完毕才输出,说明传入then的回调函数是异步执行,同理catch也是一样;

异步函数调用链

thencatch都是Promise的实例方法,都返回一个新的Promise,因此可以轻而易举地实现链式编程,比如上面的例子中“把每次消费剩余的钱”传给下一个函数可以改写成这样:

....//前面省略

     .then(function(dollars){  
           return buyHouse(dollars);
        }).then(function(d1){
            return buyCar(d1);
        }).then(function(d2){
            return marry(d2);
        }).then(function(d3){
            return makeBaby(d3);
        }).catch(function(result){
           //继续吃土
        });

看到这里你可能认为前一个then回调函数的返回值是后一个then的回调函数的入参,但这是不准确的,因为当then回调函数返回的是个Promise对象时,这个Promise对象到终态时后一个then才会执行,并且该Promise对象执行resolve时的入参才是后一个then的回调函数入参;

此时有必要对Promise的一个类方法resolve做以下说明,它的特性两句话:

  1. 如果传入的是个Promise对象,则直接返回这个Promise

  2. 如果是其他任何一个值(包括Error对象和undefined)则直接转换为一个resolved状态的Promise对象;

比如说下面的代码:

//以下的p1和p2逻辑上等同
    let p1 = Promise.resolve(1);
    let p2 = new Promise(function(resolve, reject) {
        resolve(1);
    });
    
    //以下的p3和p4等同
    let p3 = new Promise(function(r, j) {});
    let p4 = Promise.resolve(p3);
    
    console.log(p3 == p4); //true
    console.log(p3 === p4); //true
    
    //以下三者逻辑上等同
    Promise.resolve().then(function(dollars) {
        return 1 + 1;
    }).then(function(v) {
        console.log(v);
    });
    Promise.resolve().then(function(dollars) {
        return new Promise(function(r, j) { r(1 + 1) });
    }).then(function(v) {
        console.log(v);
    });
    Promise.resolve().then(function(dollars) {
        return Promise.resolve(1 + 1);
    }).then(function(v) {
        console.log(v);
    });

我们可以利用Promise异步执行结果传出的机制和then的链式调用,将层层嵌套的函数调用变为通过then顺序连接的链式调用
从写法和形式上看是不是人性很多呢?

通过Promise实现的链式异步函数调用,以斐波那契数列举例如下:

//一个异步的斐波那契计算
function fibonacci(v) { 
    return new Promise(function(resolve, reject) {  //每一个异步调用都返回了一个Promise
        setTimeout(function() {
            console.log(`${v.a}`);
            [v.a, v.b] = [v.b, v.a + v.b];
            resolve(v);
        }, 500);
    });
}

//以下两者逻辑等同,每个then都等待上一个promise的结果形成一条链。

// fibonacci({ a: 0, b: 1 })
//     .then(fibonacci)
//     .then(fibonacci)
//     .then(fibonacci)
//     .then(fibonacci)
//     .then(fibonacci)
//     .then(fibonacci);

Promise.resolve()
    .then(() => fibonacci({ a: 0, b: 1 }))
    .then(fibonacci)
    .then(fibonacci)
    .then(fibonacci)
    .then(fibonacci)
    .then(fibonacci)
    .then(fibonacci);

相关推荐