详解promise

缘由

最近经常上ife review别人的代码

看到了,某个大神写的使用Promise 异步使用Ajax 加载图片的代码,觉得挺牛逼的,就准备开始好好研究Promise ,写写博客啥的了

首先对于异步编程,我们几乎第一个时间可以想起的是 回调,

例如,当A事件完成后,B事件才能进行,然后C事件 , D事件

于是乎,我们应该能马上写出这样的程序:


var a = function ( callback ) {
    // 实现一些功能
    setTimeout(function () {
        console.log("现在在执行a函数,请loading");
    },1000);
    callback();
}

var b = function ( callback ) {
    // 实现一些功能
    setTimeout(function () {
        console.log("现在在执行b函数,请loading");
    },2000);
    callback();
}

var c = function ( callback ) {
    // 实现一些功能
    setTimeout(function () {
        console.log("现在在执行c函数,请loading");
    },3000);
    callback();
}

var d = function ( callback ) {
    // 实现一些功能
    setTimeout(function () {
        console.log("完成");
    },4000);
}


a(function () {
    b(function () {
        c(function() {
            d();
        })
    })
})

上面实现了,简单的以回调机制 加 setTimeout 模拟异步编程(加setTimeout 是为了让大家能够看的更加清晰)

单单4层,已经让人觉得挺复杂了,如果是10个或是更多功能需要处理呢?难道要写n个回调函数吗!!

例如这样的

callback-hell

实在是惨不忍睹,于是乎,开发人员就为这种现象起了个形象的名字 回调地狱

解决回调地狱

解决回调地狱,有许多种方法,例如,下面参考了张鑫旭老师写的ES6 JavaScript Promise的感性认知一文中男神娶女神的案例。

而且正好最近看了部电影 《杀死比尔》 挺不错的, 所以我也试着自己实现了一下新娘杀死比尔的过程,

var M = Math;
var r = M.random;

var bride = {
    job : "killer",
    age : 28,
    weapon : "katana",
    kill : function ( result ) {
        // 每一步,成功执行的概率是0.8
        if (r() > 0.2) {
            result.targetBekill( result.name )
        } else {
            result.brideBekill( result.name );
        }
    }
}

var revenge = function ( revengeList , killBill ) {
    var run = function () {
        if ( revengeList.length !== 0 ) {
            bride.kill({
                name : revengeList.shift(),
                targetBekill : function ( tname ) {
                    console.warn( "新娘成功击杀 " + tname );
                    run();
                }.bind(this),
                brideBekill : function ( tname ) {

                    console.error( "复仇失败,新娘被 " + tname + " 击杀!");
                    return;
                }.bind(this)
            })
        } else {
            killBill();
        }
    }

    run();
}

revenge(["Vernita Green" ,"O-Ren Ishii" , "Budd" , "Elle Driver" ] , function () {
    bride.kill({
        name : "bill",
        targetBekill: function () {
            console.info( "新娘复仇成功,杀死比尔" );
        },
        brideBekill: function () {
            console.error( "新娘失败,被杀死" );
        }
    })
});

上文这种方法的本质就是,不断的将下一需要完成的事件进行递归调用,但是众所周知,递归调用十分占用内存,这样弄,需要实现的功能一旦多起来,也是不太可取的。

那么有什么方法能让妈妈再也不担心我不能好好的使用异步编程呢?

Promise 对象

在ECMAScript 2015(ES6) 规范中就给我们提供了这样一个对象

Promise对象

实在话,Promise 实际上就是一个状态机,允许我们根据不同的状态提供不同的解决方案

每个Promise实例 一共有三种可能地状态:pending(准备)、fulfilled(完成)、rejected(拒绝)

根据不同状态,会调用之后的then函数 或 catch 函数。

我们可以这样声明一个Promise的实例


new Promise (function (resolve , reject) {
    // 这里是代码


    resolve("成功的结果");
    reject(new Error("xxxx"));
})

其中,resolve 和 reject 是两个用于传递结果的函数

若,代码无误通过resolve传递给下面的then , 若有误 执行reject 抛出函数

caveats:

① 代码无需写在这两个函数之前,写在其后的代码依然会执行
② resolve 和 reject 有且只能传递一个参数,其他的传递的参数会被舍弃
③ 当Promise 被声明后,就会立刻执行,中途也无法停止
④ 状态一旦 从 准备态 变成 完成 或 拒绝态之后 , 就永远不会再发生改变

then 和 catch

Promise 中,then 和 catch 两个函数,分管着 后续回调 和 错误处理,是对好基友

如果,之前的程序执行无误,就可以后续的执行then

如果,之前的程序执行出现错误,就可以将错误抛给 后续的 catch 由它进行处理

当一个Promise 里面的程序执行完成之后,状态就会改变,于是Promise会根据状态来决定后续使用then还是catch


;(function () {
    new Promise(function ( resolve , reject ) {
      //x  // 如果将x 解注释 就会出现错误, 走catch 那条路线
      resolve("程序成功进行");
      reject();
    }).then(function(val) {
        console.log(val)  // 程序成功进行
    }).catch(function (err) {
       console.log(err); // ReferenceError: x is not defined(…)
    });
})

then 和 catch 都会返回一个新的Promise 对象,新的Promise 对象又继续使用then 和 catch 处理后续程序

有时候我们可以使用then 替代 catch 的功能

因为then 是可以传递的两个参数的,名字分别叫 onfulfilled 和 onRejected

then( onfulfilled [, onRejected] )

onfulfilled 必须填写,确定成功之后做的事

onRejected 选填,确定失败之后做的事

所以我们可以这样的使用 then 来替代 catch的工作


then(null , function(err) {
    console.log(err);
});

// 等同于

catch(function(err) {
    console.log(err);
})

但是,并不建议使用then 替代catch,长的丑

调用then 和 catch 可以有两种方法

then 和 catch 调用结束后会返回一个新的 promise 实例,所以我们可以使用链式的方式来处理 异步需求

// 例如这样的代码

then()
.then()
.then()

。。。

then 内的匿名函数的返回值会被添加到 then 返回Promise 中,以达到迭代的效果


// 伪代码

then(function ( ) {
    // 计算结果a

    return a; // 返回a
}).then(function ( val ) {
    // 计算结果b
    
    console.log(val + b); // a + b
})

来看个累加的小栗子

;(function () {
    new Promise(function ( resolve , reject ) {
        var x = 5,
            y = 6; 

        resolve(x+y);
    }).then(function ( val ) {
        var z = 7;

        return val;
    }).then(function ( val ) {
        var v = 8;

        val += v;
        return val;
    }).then(function ( val ) {
        var k = 9;
        val += k;
        console.log("sum result is " +val) // 28
    });
})()

then 除了链式调用法外,还可以这样调用


;(function () {
    var p = new Promise(function ( resolve , reject ) {
        console.log("天才第一步");
        resolve("      ————Owen 代言");
    });

    p.then(function (val) {
        console.log("雀氏纸尿裤");
    });

    p.then(function (val) {
        console.log("天才第二步");
    })

    p.then(function (val) {
        console.log("还是纸尿裤");
        console.log(val)
    })
})()

这种方法无需显示的使用return 将每一步计算的结果返回,只要修改val 就可以

一个小栗子

光介绍定义挺没意思的,让我们来个Owen告白的小栗子,并且简单的分析一下Promise的执行过程

;(function () {
    "use strict";

    // 新建一个promise 
    var Owen = new Promise(function ( resovle , reject ) {
        console.log("Owen 想对 Zyz 说 I love Zyz");
        
        // 执行无误 状态由pending态 转为fulfilled
        // 开始传递执行结果
        resovle(["I love Zyz"] , "我是路人甲"); //只能传递一个参数,所以路人默默的走开了
        reject( throw Error("Owen 太紧张") );
    });

    Owen.then(function ( val ) {
        console.log("Zyz 听到了 Owen喊的" + val[0]);
        console.warn("Zyz 脸红了");
        val.push("成功");
    },function ( errorVal ) {
        //console.log( errorVal ) // 并没有打印上级传递的 ["Owen 太紧张"]
                                  // 但是会抛出了具体的代码错误
        // 处理失败的情况
        console.error( "因为Owen 太紧张了,所以告白失败" );
    });

    Owen.then(function (val) {
        // 传递的val 数组会迭代下来
        console.info("告白 " + val[1]);
    });
})()

这个程序中,是需要实现三个功能

① 是 Owen产生告白的想法 ② 处理Owen告白的过程 ③ 处理Owen告白的结果

Promise 一开始是处于pending(等待)的状态

一开始,我们对 Promise 传入的函数处理第一个功能,也就是激发”Owen 想告白”这一事件

当这个函数执行完毕没有错误,那么Promise的状态就从pending 转化为 fulfilled (完成)

当确定是fulfilled,Promise 就开始允许我们像下一步传上一步执行的结果,传递方法,是通过 给予的 resovle 函数传参进行, 这个参数 有且只允许传递一个,而传递的多余参数,则会丢失

当第一步完成之后,Promise 的状态就不会再改变了。

然后,Promise 就会执行接下来的一个个Owen.then函数,

成功的栗子 shotpic

当然,有成功就会有失败,如果中间的代码出现了一些问题就会 Promise 的状态就会转化为rejected(拒绝),之后就会执行 下一个then的onRejected事件

只要我们提前在onRejected 中写好出错时,进行的处理,那么代码,就能继续跑起来

失败的栗子 shotpic

resolve 传递 Promise

如果 resolve 传递的不是一个普通值,而是一个Promsie 对象,就可以做一些有趣的事儿

例如这个例子:

;(function () {
    var p = new Promise(function ( resolve , reject ) {
        setTimeout( function () {
            console.log("success...");
            resolve("process");
        } , 3000);
    });

    var p2 = new Promise(function ( resolve , reject ) {
        setTimeout( function () {
            console.log("loading...");
            resolve(p); // 此时p 并没有执行完,所以p2就会一直等待
        } , 1000);
    });

    // 当p 执行完成后 p2 就会通过resolve 将p 传递给 p2.then
    // p2.then 立即执行p 
    // 但是此时p 的状态是已执行完成了 那么只会执行p 下的resolve
    // 那么p2.then 的 onfulfilled 函数获得的是 p resolve传递过来的参数 
    p2.then(function (r) {
        setTimeout(function () {
            console.log(r + " is end!");
        },1000);
    })
})()

从上面的栗子,我们已经可以看出,若传递的是process 实例 ,那么传递的process 实例,会影响之后的promise 的使用

这样,我们就可以,将两个 异步运算链接在一起

Promise.resolve 方法

Promise 下 也有一些静态方法可以使用

例如这个 Promise.resolve 就会把,传入的普通类型的参数,返回出来一个Process 实例

var createPromise = Promise.resolve("Owen");

console.log(createPromise) 
// Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: "Owen"}}

可以看出,Promise.resolve 的作用是实例化将一个promise对象 并将状态立即转为 resolved

并给 resolved 函数 传入 参数

所以上面的代码 ,就等同于这样

var createPromise = Promise.resolve("Owen");

// 等同于

var createPromise = new Promise(function (resolved) {
    resolved("Owen");
})

有了这个函数,我们就可以这样写代码了

Promise.resolve(1)
    .then(function(val) {
        return ++val; 
    })
    .then(function(val) {
        return ++val; 
    })
    .then(function(val) {
        return ++val; 
    })
    .then(function(val) {
        console.log(++val); 
    })

除此之外,Promise.resolve 还有一个功能,就是接受 一个带有 then 方法的对象

+function () {
    var obj = {
        then : function (resolve , reject) {
            var a = 1;
            var b = 2;

            resolve(a+b);
        }
    }


    p = Promise.resolve(obj);
    p.then(function (val) {
        var c = 3;
        console.log( c * val ); // 9
    })
}()

这个功能的两个限制条件

① 传递的是一个对象
② 对象中有一个叫then 方法

所以,像如下代码,是无法使用这个功能的

+function () {
    var then = function (resolve , reject) {
        var a = 1;
        var b = 2;

        resolve(a+b);
    }

    var arr = [then];

    p = Promise.resolve(arr);
    p.then(function (val) {
        var c = 3;
        console.log( c * val ); // NaN
    })
}()

// or...

+function () {
    var obj = {
        Then : function (resolve , reject) { // 大写
            var a = 1;
            var b = 2;

            resolve(a+b);
        }
    }


    p = Promise.resolve(obj);
    p.then(function (val) {
        var c = 3;
        console.log( c * val ); // NaN
    })
}()

caveats:

如果promise 得到的是一个 promise 实例,那么就不会做任何改动
如果promise 没有得到参数,也不会报错,就是 resolve 不会传递任何参数

Promise.reject

和上面的 Promise.resolve 一样,相当于传递时reject


var p = Promise.reject('出错了'); // Uncaught (in promise) 出错了

////////// 等同于 //////////// 

var p = new Promise(function(resolve , reject) {
    reject('出错了');
})

p.catch(function (err){
  console.error(err) 
});

Promise.all 和 Promise.race

Promise.all 可以处理多个传入的Promise 实例

var p1 = new Promise ( function (resolve , reject) {
    var a = 1,
        b = 2;

    resolve( a + b );
});

var p2 = new Promise ( function (resolve , reject) {
    var a = 3,
        b = 4;
    // 如果其中一个发生了什么错误
    resolve( a + b );
    reject("报错了");
});

var p3 = new Promise ( function (resolve , reject) {
    var a = 5,
        b = 6;

    resolve( a + b );
});

Promise.all([p1 , p2 , p3]).then(function (val) {
    console.log(val) // [3, 7, 11]
}).catch(function (err) {
    console.error(err); // 报错了
}) 

all 的意思就是等大家都到齐了,再继续执行下一步

当然不免也会有报错的情况发生,如果其中有一个错误,那么将其中一个错误报至catch

当然,不一定传入的参数非要是promise 实例,即使不是promise实例,Promise.all也会按照Promise.resolve将传入的参数转化为 promise实例

var p = [2, 3, 5, 7, 11, 13].map(function (value , idx) {
  return Math.pow(value , 2);
});

Promise.all(p).then(function (val) {
    console.log(val); // [4, 9, 25, 49, 121, 169]
}).catch(function(err){
  console.error(err);
});

摘录[阮一峰 Promise对象]: Promise.all方法的参数可以不是数组,但必须具有Iterator接口 Process.race 的话,会监听到底哪个参数率先变化了状态,并将那个变化状态了的promise 返回给then

var p1 = new Promise ( function (resolve , reject) {
    
    setTimeout(function () {
        var a = 1,
            b = 2;

        resolve( a + b );
    },1000); // 这个promise 是速度最快的,所以率先执行它
    
});

var p2 = new Promise ( function (resolve , reject) {
    
    setTimeout(function () {
        var a = 3,
            b = 4;
        // 如果其中一个发生了什么错误
        resolve( a + b );
        reject("报错了");
    },2000)

});

var p3 = new Promise ( function (resolve , reject) {

    setTimeout(function () {
        var a = 5,
            b = 6;

        resolve( a + b );
    },3000);
   
});

Promise.race([p1 , p2 , p3]).then(function (val) {
    console.log(val) // 3
}).catch(function (err) {
    console.error(err); // 报错了
}) 

模拟Sleep函数

Js中是没有类似C 和 C++ 一样的 sleep函数的

但是我们可以使用promise对象来模拟sleep函数

function Sleep (timeout) {
    return new Promise(function(resolve) {
        setTimeout(function () {
            resolve();
        },timeout);
    });
}

有了sleep函数我们做Css3的流程动画的时候就比较方便了

new promise(function resolve) {
    // 动画一 修改类
    
    reslove();
}).then(function () {
    // 动画二 修改类

    return Sleep(2000); // 等待2秒后
}).then(function () {
    // 动画三 修改类

})

感谢

阮一峰 Promise对象

ES6 JavaScript Promise的感性认知

MDN Promise

JS魔法堂:剖析源码理解Promises/A规范

- CATALOG -