听风是风

学或不学,知识都在那里,只增不减。

导航

es6入门4--promise详解

 

可以说每个前端开发者都无法避免解决异步问题,尤其是当处理了某个异步调用A后,又要紧接着处理其它逻辑,而最直观的做法就是通过回调函数(当然事件派发也可以)处理,比如:

请求A(function (请求响应A){
  //请求响应A作为参数调用方法B
  funB(请求响应A);
});

但从业务角度来说,回调往往不会只有一层;例如我项目中有一个购物车结算的需求:我需要先给网站A下个单,然后以A请求返回的单号为参数调用另一个借口,以给网站B下一个回执单,回执单拿到之后才是跳转页面,大概是这样:

下单A(function (请求响应A){
  //下单A响应成功后调用下单B
  下单B(function(请求响应B){
    //下单B成功后跳转
    window.location.href = '我是订单页'
  });
});

如果请求再多点呢,通过回调的做法我们只能层层嵌套,这也就诞生了让代码维护者头痛的回调地狱。想想几个月后,你的同事或者自己重新阅读这段代码时,函数嵌套与未分离的大量业务代码,不头大都难!

而Promise的出现正好解决了这一痛点,通过promise我们能以链式调用的形式取代传统回调嵌套的写法,同时还能将逻辑代码从毁掉地狱中抽离出来,改写上面上面的例子,像这样是不是好看了很多:

new Promise(下单A)
  .then(下单B(单号A))
  .then(页面跳转)

一、基本用法

Promise强大的地方就在于能通过对异步请求状态的改变,与我们达成一种承诺;例如将请求的状态由pending(进行中)改为fulfilled(已完成),我们就可以通过then方法对应的回调处理相关后续逻辑了。

Promise对象的状态一共有三种,进行中pending已成功fulfilled(resolved)已失败rejected。值得一提的是,promise对象的状态不会受外界影响,但我们可以通过内置方法对状态进行改变。且状态一旦改变,此状态就会定型;直白点说就是假设我们将pending改为resolved后,不管什么时候再去访问它,状态将永远维持为已完成状态。我们通过一个例子证实这一点:

let fn = (resolve, reject) => {
    //改为resolved
    resolve(1);
    //再次修改状态无效
    reject(2);
};
let p = new Promise(fn);
//输出1
p.then(resp => console.log(resp), err => console.log(err));

1.创建promise实例并通过then回调

 Promise对象是一个构造函数,我们可以通过new来新建一个Promise实例:

function promiseDemo(resolve, reject) {
  if (true) {
    //异步请求成功
    resolve(1);
  } else {
    reject(respError);
  };
};
let promise = new Promise(promiseDemo);
console.log(promise);

在new的操作中,我们需要给Promise传递一个函数作为参数,且此函数中可直接使用resolve与reject参数,用于修改异步请求的状态。

在上述代码中,我们假设发起了一次异步请求,同时拟定true为异步请求成功,通过resolve处理请求成功的返回数据,得到了一个promise实例。通过打印可以看到状态被修改为已成功(resolved),同时得到了响应成功数据value为1;

现在尝试使用then处理回调,可以看到成功输出了1,而这个1是我们拟定异步请求成功返回的数据。

promise.then(function (resp){
  //异步请求成功后续逻辑
  console.log(resp);//1
},function (resprror){
  //请求失败逻辑
});

 那上面就是一个模拟的简单的promise例子了,下面具体聊聊promise内置方法,看看promise具体有哪些用法。

二、Promise方法介绍

1.Promise.resolve()

Promise.resolve()能将一个对象转为promise对象。

我在前面说,通过new Promise能得到一个promise实例对象,其实通过Promise.resolve()也能创建一个promise实例,下面这个例子可以看到得到的结果是相同的:

let promise1 = Promise.resolve(1);
let promise2 = new Promise(resolve => resolve(1));
console.log(promise1, promise2);

 需要注意的是Promise.resolve()接受的参数不同,处理参数的行为也将不同。

1.1传递一个promise对象作为参数

如果我们为Promise.resolve()传递一个promise对象,那么它将会将此对象原封不同的返回:

let promise1 = Promise.resolve(1);
let promise2 = Promise.resolve(promise1);
console.log(promise1 === promise2);//true 还是原来的味道

当我们使用promise作为参数用于创建另一个promise对象时,还需要注意promise具有状态传递的特性:

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => reject(1), 3000)
});

const p2 = new Promise((resolve, reject) => {
    setTimeout(() => resolve(p1), 1000)
});
//等待四秒后触发reject回调,输出失败了
p2.then(resp => console.log('成功了'),err => console.log('失败了'));

上述代码中,p2的状态由p1决定,四秒后p1 then方法还是触发了失败回调。

1.2传递一个非对象,例如数字,字符串

当我们传递一个非对象作为参数,Promise.resolve会返回一个promise对象,状态为resolved,且通过then回调我们能正常访问该参数。

let promise1 = Promise.resolve(1);//第一步执行
console.log(promise1);//第二步执行
promise1.then(resp => console.log(resp));//then方法最后执行
console.log(2);//第三步执行

在ES6入门这本书中说,由于传递的参数不具备异步行为(不带有then方法),所以Promise.resove同步执行修改了参数状态,并立刻执行then回调;但事实并非如此;如上,通过断点将执行先后步骤加在了注释里,打印2的操作要早于then,then依旧是最后执行,这里特别指出。

我对于这里的理解是,不管是new Promise()中的resolve()还是Promise.resolve(),只要不具备异步行为,resolve方法本身都将同步执行,但是then回调仍然是异步触发。

一个new Promise例子:

let demo = resolve => {
  resolve(1);
};
let promise = new Promise(demo);
//此时的promise状态已修改为resolved,说明resolve(1)已触发
console.log(promise)
promise.then(resp => {
  console.log(resp);
});
console.log(2);

这个例子中依次打印promise,2,1。且打印出的promise对象状态为resolved,说明resolve()方法为同步执行,但then回调最后触发。

一个Promise.resolve()例子:

let promise = Promise.resolve(1);
console.log(promise);
promise.then(resp => console.log(resp)) console.log(2
);

依旧是第一打印promise,第二次打印1,且promise状态是resolved,说明在打印1之前,Promise.resolve(1)已经执行完成,then回调最后触发执行。

这里我加个例子与上面的代码做对比,让resolve处理异步操作,下面的代码才符合resolve完成立刻触发then回调的情况:

let demo = resolve => {
  console.log(1);
  setTimeout(() => {
    resolve(4);
  }, 1000);
  console.log(2);
};
let promise = new Promise(demo);
promise.then(resp => console.log(resp));
console.log(3);

上述代码依次会输出1,2,3,4,这个例子中resolve()方法由于异步的问题等到同步代码跑完了才触发了,同时resolve完成立刻触发了then方法,最后输出了4。

1.3.传递一个thenable对象

thenable对象是指带有then方法的对象,我们可以手动创建此类对象,我个人感觉angular中$http返回的对象应该也是thenable对象。

对于thenable对象resolve方法会将此对象转为promise对象,得到的实例也能正常通过then方法回调。

let thenable = {
  then: (resolve, reject) => resolve(42)
};
let p1 = Promise.resolve(thenable);
p1.then(value => console.log(value));//42

1.4.不传递参数

如果不传递参数,则得到一个没有value,但状态是resolved的promise对象。

let promise = Promise.resolve();
console.log(promise);

2.Promise.reject()

Promise.reject()也会返回一个promise实例,状态为rejected。reject接收的参数会原封不动作为reject回调时的参数,不像resolve那么多情况。

let thenable = {
    then: function (resolve, reject) {
        reject(1);
    }
};
let p = Promise.reject(thenable);
p.then(resp => {
    console.log(resp)
}, err => {
    console.log(err === thenable)//true
});

3.Promise.prototype.then()

为什么次方法是Promise.prototype.then()而不是Promise.then()呢,这是因为then方法是为Promise实例提供,而实例的方法是通过继承而来,then方法在Promise对象的原型链上也就合情合理了。

通过前面的例子,我们也知道了then方法提供了2个回调函数,第一个对应resolved状态,第二个对应rejected状态

需要注意的是,then方法会隐性返回一个新的promise实例,我们甚至可以无限使用then回调都不会报错:

let p = Promise.resolve(1);
p.then(resp => console.log(resp)) //1
    .then(resp => console.log(resp)) //undefined
    .then(resp => console.log(resp)) //undefined
// ...无数个

也正因为这个特性,我们在处理异步请求A的then回调中,可以手动返回一个异步请求B的promise实例,通过这样的做法也就实现了同步链式的写法:

let p1 = Promise.resolve(1),
    p2 = Promise.resolve(2),
    p3 = Promise.resolve(3);
p1.then(resp => {
    return p2;
}).then(resp => {
    return p3;
}).then(resp => {
    console.log(resp);//3
});

4.Promise.prototype.catch()

总是推荐使用catch()方法代替then方法中的第二个回调;这是因为catch方法不仅能捕获异步请求的错误,它还能捕获then方法的错误,但then的第二个回调做不到这一点。

let p = Promise.resolve(1);
p.then(resp => {
    console.log(x);
}).catch(err => {
    console.log(err);//x is not defined
});

上述代码中我们在成功回调中故意打印一个未定义的变量x,catch成功帮我们捕获了这个错误,但是如果使用then第二个错误回调,是无法捕获的。

const fn = (resolve,reject) => {
    console.log(x);
};
let p = new Promise(fn);
p.then(resp => {
    console.log(1);
}).catch(err => {
    console.log(err);//x is not defined
});

这个例子中,我们在创建promise实例的函数中故意出错,catch也捕获了错误,虽然这个错误then第二个回调也能做到,但整体来说catch更为强大,这也是推荐使用catch而不是then第二个回调的理由。

说到处理错误,Promise还有个奇怪的地方,假设Promise出错了,但没使用then第二回调或者catch处理错误;尽管程序会报错,但这个错误并不会抛出给外层,所以外层程序并不会因此停止执行,所以在then回调后面跟一个catch方法是有必要的。

const fn = (resolve, reject) => {
    resolve(1);
};
let p = new Promise(fn);
p.then(resp => {
    console.log(x);
});
setTimeout(() => {
    console.log(1);
}, 0);

5.Promise.prototype.finally()

finally()方法有点像switch case中的default,不管你异步成功了还是失败了,finally都会如约而至的触发。一般用法是这样:

const p = Promise.resolve(1);
p.then(resp => console.log(resp))
    .catch(err => console.log(err))
    .finally(() => console.log('执行完毕'));

finally方法因为不关心Promise状态,所以不需要传递参数,在ES6入门中也提到,由于不管状态成功或者失败都会触发,所以finally也等同于then方法中使用两个回调的做法。

6.Promise.all()

Promise.all()方法接受多个promise实例,返回一个全新的promise实例:

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

如果p1,p2,p3不是promise实例,则会在all执行前先为这三个参数执行Promise.resolve()方法。

all方法返回的promise实例的状态由参数共同决定,以上面代码为例,如果三个promise实例状态全部为resolved,则p的状态便为resolved,但如果三个实例有一个未rejected,则p的状态便为rejected。

//全部为resolved
let p1 = Promise.resolve(1),
    p2 = Promise.resolve(2),
    p3 = Promise.resolve(3);
let p = Promise.all([p1, p2, p3]);
p.then(resp => console.log(resp));//[1,2,3];

//部分为reject
let p1 = Promise.resolve(1),
    p2 = Promise.reject(2),
    p3 = Promise.reject(3);
let p = Promise.all([p1, p2, p3]);
p.then(resp => console.log(resp))
.catch((err) => console.log(err));//2;

这里有个需要注意的地方,如果一个状态为rejected的promise实例使用了then方法的第二回调,或者使用了catch()方法,这会导致all()无法触发自己catch()方法或者then的第二回调。

let p = Promise.reject(1).then(resp => resp, err => err);
// 或者
// let p = Promise.reject(1).then(resp => resp)
//     .catch(err => err);
Promise.all([p])
    .then(resp => console.log('成功执行'))//成功执行
    .catch(err => console.log('报错啦'));

上述例子中创建promise对象时虽然使用了reject方法,但由于自身有捕获错误的操作,导致实例p拿到的是then方法返回的另一个promise对象。我们可以看看状态:

let p = Promise.reject(1).then(resp => resp, err => err);
console.log(p);

不管什么时候都应该记住,then方法也会返回一个新的promise对象。

 7.Promise.race()

 Promise.race()同样是接受多个promise实例返回一个全新promise实例的方法,但与all方法不同的地方在于,决定这个promise状态的是多个实例参数中最先改变状态的那个。

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

假设p3最先改变状态成了rejected,那么p的状态也就是rejected。p1,p2随后再改变状态将不会对p实例起作用。

let p1 = Promise.reject(1);
let p2 = Promise.resolve(resole => {
    setTimeout(() => {
        resole(2);
    }, 3000)
});
Promise.race([p1, p2])
    .then(resp => console.log(resp))
    .catch(err => console.log(err));//1

上述例子中p1是一个同步执行状态为rejected的promise实例,p2是异步创建状态为resolved的实例,由于p2需要等三秒,所以最终race的实例以p1为主,这里最终输出1。

那么到这里promise基本方法就介绍完了,一个个例子去理解以及自己钻牛角尖也花了一点时间,接下来应该会写下promise执行顺序的文章,这个与宏任务微任务挂钩,还有就是手写promise需要看下,那么这篇文章就写到这里。

如果对于JS执行机制相关有疑问,可以阅读博主这篇博文

JS执行机制详解,定时器时间间隔的真正含义

欢迎大家留言讨论!!!

posted on 2019-04-21 23:33  听风是风  阅读(1650)  评论(0编辑  收藏  举报