ES6 Promise 对象(学习笔记)

1. Promise 的两个特点

(1)对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这个也是 Promise 这个名字的由来,代表只要“承诺”了便无法改变。

(2)一旦状态确定,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态只有两种情况,从 pending 到 fulfilled 和从 pending 到 rejected。只要这两种情况发生了,状态就凝固了,会一直保持这个结果,这时就成为 resolved(已定型)。如果已定型,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是如果你错过了它再去监听,是没有结果的。

有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得异步操作更加容易。

但是,Promise 对象也有一定的缺点:首先,无法取消 promise,一旦建立它就会立即执行,无法中途取消。其次,如果不设置回调函数,promise 内部抛出的错误不会反应到外部。第三,当处于 pending 状态时,无法得知目前进展到哪个阶段(是刚刚开始还是即将完成)。

若某些事件反复地发生,一般来说,使用 stream 模式是比部署 Promise 更好的选择。

2. 基本语法

ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例

const promise = new Promise(function(resolve,reject){//函数里的两个参数由 JavaScript 提供,不用自己部署
     //some code
     if(/*异步操作成功*/){
         resolve(value);
     }else{
         reject(error);
     }
});

Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数

then 中的两个参数,第二个可选,都接受 promise 对象传出的值作为参数
promise.then(function(value){
   //success
},function(error){
   //failure
});

下面是一个简单的 Promise 对象的例子

function timeout(ms){
   return new Promise((resolve,reject) => {
      setTimeout(resolve,ms,'done');
   });
}

timeoue(100).then((value) =>{
   console.log(value);
});

Promise 一旦建立就会立即执行,then 方法指定的回调函数,在当前脚本所有同步任务执行完后才会执行。

下面是加载图片的例子

function loadImageAsync(url){
   return new Promise(function(resolve,reject){
        const image = new Image();

        image.onload = function(){
            resolve(image);
        };
        image.onerror = function(){
            reject(new Error("could not load image at"+url));
        };

        image.src = url;
   });
}

resolve 函数和 reject 函数的参数会传递给回调函数,它们的参数也可以是一个 Promise 对象,即一个异步操作的结果是返回另一个异步操作。

const p1 = new Promise(function(resolve,reject){
     setTimeout(() => reject(new Error('fail')),3000);
});

const p2 = new Promise(function(resolve,reject){
    setTimeout(() => resolve(p1),1000);
});

p2
  .then(resolve => console.log(resolve))
  .catch(error => console.log(error))

//Error:fail

/*由于 p2 返回的是另一个 Promise 的状态,导致 p2 自己的状态无效了,由 p1 的状态控制 p2 的状态,
所以后面的 then 语句和 catch 语句都变成针对 p1 了。执行时,p1 3 秒之后变为 rejected ,p2 的状态
在 1 秒后改变,返回 p1 。又过了 2 秒后, p1 变为 rejected,导致触发 catch 方法指定的回调函数。*/

若 Promise 对象中,resolve 方法(reject 同)后面还有语句,则后面的语句会先执行,这是因为 resolve 总会晚于本轮循环的同步任务。所以我们最好在 resolve 前面加上 return 语句,这样就不会有意外

//例1
const p = new Promise(function(resolve,reject){
    resolve(1);
    console.log(2);
}).then(r => {
    console.log(r);
});

//2
//1

//例2
const p = new Promise(function(resolve,reject){
    return resolve(1);  
    console.log(2);     //不会执行
});

3. Promise.prototype.then():then 方法的第一个参数是 resolved 状态的回调函数,第二个参数(可选)是 rejected 状态的回调参数。一般来说,不要在 then() 方法里面定义 Rejected 状态的回调函数(then 的第二个参数),总是使用 catch 方法比较好。

4. Promise.prototype.catch():这个方法是 .then(null,rejection) 或 .then(undefined,rejection) 的别名,用于指定发生错误时的回调函数。Promise 对象的错误具有 “冒泡” 性质,会一直向后传递,直到被捕获为止。

getJson('/posts.json').then(function(posts){
    //...
}).catch(function(error){
    //处理 getJson 和前一个回调函数运行时发生的错误
    console.log("发生错误! ",error);
});

reject()的作用,等同于抛出错误。

const promise = new Promise(function(resolve,reject){
     throw new Error('test');
});

promise.catch(function(error){
     console.log(error);
});

//Error:test

//等同于
const promise = new Promise(function(resolve,reject){
     try{
          throw new Error('test');
     }catch(e){
          reject(e);
     }
});

promise.catch(function(error){
     console.log(error);
});

//等同于
const promise = new Promise(function(resolve,reject){
     reject(new Error('test'));
});

promise.catch(function(error){
     console.log(error);
});

跟传统 try/catch 代码块不同的是,若没有使用 catch()指定错误处理的回调函数,Promise 对象发生的错误不会传递到外层代码,即不会做出任何反应

const someAsyncThing = function(){
    return new Promise(function(resolve,reject){
        //下面一行信息报错,因为 x 没有声明
        resolve(x + 2);
    });
};

someAsyncThing().then(() => {console.log('everything is great')});

setTimeout(() => {console.log(123)},2000);

// Uncaught (in promise) ReferenceError: x is not defined
// 123

以上代码说明,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“ Promise 会吃掉错误”。这个脚本放在服务器执行,退出码就是 0 (即表示执行成功)。

不过,Node.js 有一个专门监听未捕获的 reject 错误的事件unhandleRejection,可以在监听函数里面抛出错误。第一个参数是错误对象,第二个参数是报错的 Promise 实例,它可以用来了解发生错误的环境信息。

process.on('unhandleRejection',function(error,p){
    throw error;
});

注意,Node 有计划在未来废除 unhandleRejection 事件。如果 Promise 内部有未捕获的错误,会直接终止进程,并且进程的退出码部位 0。

5. Promise.prototype.finally():用于指定不管 Promise 对象状态最后如何,都会执行的操作。finally() 不接受任何参数。

以下一个例子是服务器使用 Promise 处理请求,最后使用 finally 方法关掉服务器。

server.listen(port)
  .then(function(){
      //...
  })
  .finally(server.stop);

6. Promise.all():用于将多个 Promise 实例,包装成一个 Promise 实例。其参数可以不是数组,但必须是 Iterator 接口,且返回的每个成员都是 Promise 实例。

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

其中 p1,p2,p3 都是 Promise 实例,若不是实例,就会先调用 Promise.resolve(),将参数转成 Promise 实例。

p 的状态由 p1,p2,p3 决定

(1)只有当 p1,p2,p3 的状态都为 fulfilled , p 的状态才会为 fulfilled,此时 p1,p2,p3 的返回值组成一个数组,传递给 p 的回调函数

(2)只要 p1,p2,p3 中有一个状态为 rejected,p 的状态就会为 rejected,此时第一个被 rejected 的实例的返回值,会传递给 p 的回调函数。 

注意,若作为参数的 Promise 实例,自己定义了 catch 方法,那么它一旦被 rejected ,就不会出发 Promise.all() 的 catch 方法。

const p1 = new Promise(function(resolve,reject){
    resolve("hello");
})
  .then(result => result)
  .catch(e => e);

const p2 = new Promise(function(resolve,reject){
    throw new Error("报错了");
})
  .then(result => result)
  .catch(e => e);

Promis.all([p1,p2])
  .then(result => console.log(result))
  .catch(e => console.log(e));

//["hello",Error:报错了]

//p1 的状态为 resolved,p2 实例执行完 catch 后,状态也会变成 resolved,故 Promise.all() 的状态也是 resolved ,因此会调用 then()指定的回调函数。

若 p2 没有自己的 catch 方法,就会调用 Promise.all() 的 catch 方法。

const p1 = new Promise(function(resolve,reject){
    resolve("hello");
})
  .then(result => result)
  .catch(e => e);

const p2 = new Promise(function(resolve,reject){
    throw new Error("报错了");
})
  .then(result => result);

Promis.all([p1,p2])
  .then(result => console.log(result))
  .catch(e => console.log(e));

//Error:报错了

7. Promise.race():同样是将多个 Promise 实例包装成一个 Promise 实例。

Promise.race() 和 Promise.all() 一样,如果参数不是 Promise 实例,会先调用 Promise.resolve()将参数转为 Promise 实例再进一步处理。

下面是一个例子,若在指定时间内没有获得结果,就将 Promise 的状态变为 rejected,否则变为 resolved。

const p = Promise.race([
     fetch('/resource-that-may-take-a-while'),
     new Promise(function(resolve,reject){
        setTimeout(() => reject(new Error('request timeout')),3000);
     })
 ]);

p
  .then(console.log)
  .catch(console.error);
 
//若在 3 秒之内,fetch 方法无法返回结果,变量 p 的状态就会变为 rejected,从而触发 catch 指定的方法回调函数

8. Promise.allSettled():接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些实例都返回结果,无论是 fulfilled 还是 rejected,包装实例才会结束。

const promises = [
   fetch('/api-1'),
   fetch('/api-2'),
   fetch('/api-3'),
];

await Promise.allSettled(promises);
removeLoadingIndicator();

下面是返回值用法的例子。

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);

//过滤出成功的请求
const succesefulPromise = results.filter(p => p.status === "fulfilled");

//过滤出失败的请求,请输出原因
const error = results
    .filter(p => p.status === "rejected")
    .map(p => p.reason);

//每个对象都有 status 属性,当 Promise 实例的状态为 fulfilled 时,status 有 value 属性,Promise 状态为 rejected 时,status 有 reason 属性。

有时候,我们不关心异步操作的结果,只关心异步操作有没有结束。这时,Promise.allSettled() 就很有用,Promise.all() 无法做到这一点。

const urls = [/*...*/];
const requests = urls.map(x => fetch(x));

try{
   await Promise.all(requests);
   console.log("所有请求都成功");
}catch{
   console.log("至少一个请求失败,其他请求可能还没结束");
}

9. Promise.any():接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个是 fulfilled 状态,那么包装实例就会变成 fulfilled 状态,参数实例全部都为 rejected 状态时,包装实例才会为 rejected 状态。

Promise.any() 与 Promise.race() 很相似,只有一点不同,就是不会因为某个 Promise 状态为 rejected 而结束。

Promise.any() 抛出的错误不是一个一般的错误,而是一个 AggregateError 实例,它相当于一个数组,每个成员对应一个被 rejected 的操作所抛出的错误。

new AggregateError() extends Array -> AggregateError

const err = new AggregateError();
err.push(new Error('first error'));
err.push(new Error('second error'));
throw err;

下面是一个例子

var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
var rejected2 = Promise.reject(Infinity);

Promise.any([resolved,rejected,rejected2]).then(function(result){
    console.log(result);  //42
});

Promise.any([rejected,rejected2]).catch(function(result){
    console.log(result);  //[-1 Infinity]
});

10. Promise.resolve():将现有对象转为 Promise 实例。

Promise.resolve('foo');

//等同于 

new Promise(resolve => resolve('foo'));

Promise.resolve() 的参数分成 4 中情况。

(1)参数是 Promise 实例,那么 Promise.resolve() 将不做任何修改,原封不动的返回这个实例。

(2)参数是一个 thenable 对象(即具有 then()的对象),将其转为 Promise 实例之后,立即执行 thenable 对象的 then 方法

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

let p1 = Promise.resolve(thenable);
p1.then(function(value){
   console.log(value);  //42
});

(3)参数不是具有 then 方法的对象,或者根本不是对象,则 Promise.resolve() 返回一个新的对象,状态为 resolved。

const  p = Promise.resolve('hello');

p.then(function(s){
   console.log(s);  //hello
});

(4)不带任何参数,直接返回一个 resolved 状态的 Promise 对象。若希望得到一个 Promise 对象,最简单的方法就是用 Promise.resolve()

需要注意的是,立即 resolve()的 Promise 对象,是在本轮事件循环(event loop)的结束时执行,而不是在下一轮事件循环的开始时。

setTimeout(function(){
   console.log("three");
},0);

Promise.resolve().then(function(){
  console.log('two');
});

console.log('one');

//one
//two
//three

11. Promise.reject():也会返回一个 Promise 实例,状态为 rejected

注意,Promise.reject() 的参数,会原封不动的作为 reject 的理由,变成后续方法的参数,这一点与 Promise.resolve() 不同。

const thenable = {
   then:function(resolve,reject){
       reject('出错了');
   }
};

Promise.reject(thenable)
  .catch(e =>{
     console.log(e === thenable);  //true
  });

以上代码中 Promise.reject() 的参数是一个 thenable 对象,执行以后,后面 catch 的参数不是 reject()抛出的 “出错了”,而是 thenable 对象。

12. Promise.try():让同步函数同步执行,异步函数异步执行,并且有统一的 API。

第一种写法:

const f = () => console.log('now');

(async () => f())();  //立即执行函数

console.log('next');

//now
//next

以上代码,若 f 是同步的,就会得到同步的结果,若 f 是异步的,就可以用 then 指定下一步,如

(async () => f())()
  .then(...)

需要注意的是,async() 会吃掉 f() 抛出的错误,如果想要捕获错误,要使用 promise.catch()

(async () => f())()
  .then(...)
  .catch(...)

第二种写法:使用 new Promise()

const f = () => console.log('now');

(
   () => new Promise(resolve,reject){
       resolve => resolve(f());
   }
)();

cosole.log('next');
//now
//next

鉴于这是一个很常见的需求,所以有一个提案,用 Promise.try() 代替以上的写法

const f = () => console.log('now');

Promise.try(f);

console.log('next');

//now
//next

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章