期约

基础

期约类似于事件处理。例如

let p = new Promise((resolve, reject) => {});
setTimeout(console.log, 0, p1);
timeout的三个参数第一个是执行的函数,第二个是等待的时间,后面的都是函数的参数

他和普通的timeout的区别在于它可以通过函数设置参数,从而异步决定某些函数是否执行及这些函数的参数。

期约具有三种状态: 待定(pending)、兑现/解决(resolved)、拒绝(rejected)

在待定状态下,期约可以落定,一旦跳转到解决或拒绝状态就不能再变化。

  • Promise((resolve, reject) => {…}): reslove和reject是跳转到解决或拒绝状态的函数。两个函数都可以传递一个参数,resolve的参数表示解决期约的值(感觉有点像返回值)。reject的参数表示拒绝的理由。此外,reject还会抛出一个异常
new Promise(() => setTimeout(console.log, 0, 'executor'));
setTimeout(console.log, 0, 'promise initialized');

//executor
//promise initialized

异步的执行器是同步执行的,所以executor在promise initialized前面输出。

reject抛出的异常是不能被常规的try/catch捕获的,例如

try
{
Promise.reject(new Error('bar'));
}
catch(e)
{
console.log(e);
}

//输出 Uncaught (in promise) Error: bar

then,catch

then

Promise.prototype.then()可以为期约添加处理程序。也就是receive和reject的处理程序。例如:

let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));
p1.then(()=> console.log("p1")), ()=> console.log("p1"));
p2.then(()=> console.log("p2")), ()=> console.log("p2"));
三秒后输出
p1 resolved
p2 rejected

  • Promise then(onresolve, onreject): 两个参数必须是函数类型,如果不需要onresolve可以传null。返回值是一个新的期约实例。这个期约实例是根据期约的状态创建的如果是解决状态那么会调用onresolve。如果原来是pending那么两个函数都不会执行。如果没有提供处理程序,那么会直接返回上一个期约的处理值。如果没有onResolve中没有显式的处理程序,那么会返回undefined。

新的期约默认是解决状态,如果在then中返回一个新的期约并且是pending状态那么返回也是pending状态。

let p1 = Promise.resolve('foo');//直接抛出解决的期约
let p2 = p1.then();
setTimeout(console.log, 0, p2);//Promise<resolved>: foo(没有处理程序会返回上一个期约的返回值)

let p3 = p1.then(()=>'bar');//bar
setTimeout(console.log, 0, p3);//Promise<resolved>: bar

let p4 = p1.then(()=> Promise.reject());
//Uncaught (in promise): undefined
setTimeout(console.log, 0, p4);//Promise<rejected>: undefined

他直接运行resolve函数,可以在resolve函数中决定期约的状态。如果在resolve中抛出错误,则会调用rejected函数

let p = Promise.resolve('foo');
let p1 = p.then(()=>{throw 'baz'});
//Uncaught (in promise) baz

setTimeout(console.log, 0, p1);//Promise<rejected> baz
但是如果返回错误值结果是resolved状态
let p2 = p.then(() => Error('qux'));
setTimeout(console.log, 0, p2);//Promise<resolved>: Error: qux

期约一旦落定,由这个期约所产生的期约都会延迟到当前线程同步代码的末尾执行。例如

function test()
{
let p = new Promise((resolve, reject)=>{console.log(1);resolve();});
let p2 = p.then(()=>console.log(4));
console.log(2);
}
test();
console.log(3);
输出为
1
2
3
4

如果有多个期约进入了接收或拒绝状态,那么他们后面的顺序按照代码顺序来
let p1 = Promise.resolve();
let p2 = Promise.reject();

p1.then(()=> setTimeout(console.log, 0, 1));
p2.then(()=> setTimeout(console.log, 0, 2));
//1
//2

如果前面的状态是reject,那么后面会调用onreject函数,并且返回期约的状态是resolve,因为你对reject状态进行处理

catch

catch是一个语法糖,它相当于只有onRejected处理程序

let p = Promise.reject();
let onRejected = function(e)
{
setTimeout(console.log, 0, rejected);
}

p.then(null, onRejected);//rejected
p.catch(onRejected);//rejeected
//二者相同

finally

Promise.prototype.finally()用来添加onFinally程序,他在解决或拒绝状态都会触发,但是他没法知道事解决还是拒绝状态,因此一般只用来清理代码。

和前面的then不同,它在大多数情况下都会原样后传父期约,而不会改变他。

let p = Promise.resolve('foo');

let p1 = p.finally();
let p2 = p.finally(()=>undefined);
let p3 = p.finally(()=>'bar');

setTimeout(console.log, 0, p1);
...
//三个的返回值都是Promise <resolved>: foo

如果返回待定期约或者出现错误则会返回相应的期约

期约连锁和期约合成

期约连锁是让多个期约依次执行。也就是使用多个then。

例如:

let p1 = new Promise((resolve, reject) => {
console.log('p1 executor');
setTimeout(resolve, 1000);
});

p1.then(()=> new Promise((resolve, reject)=>{
console.log('p2 executor');
setTimeout(resolve, 1000);
})).then(()=>new Promise((resolve, reject)=>{
console.log('p3');
setTimeout(resolve, 1000);
}));

这样比较直观,但是如果不使用期约也可以实现类似功能

function delay(str, callback=null)
{
setTimeout(()=>{
console.log(str);
callback && callback();
), 1000);
}

delay('p1', ()=>{
delay('p2', ()=>{
delay('p3', ()=>{
delay('p4');
});
});
});

一个契约可以由任意多个处理程序,我们可以使用有向图进行描述

  A
/ \
B C
...

Promise.all()

Promise.all会在所有期约解决之后再解决。例如

let p = Promise.all([
Promise.resolve(),
new Promise((resolve, reject) => setTimeout(resolve, 1000);
]);
setTimeout(console.log, 0, p);
p.then(()=>setTimeout(console.log, 0, 'all');

只要有一个拒绝,那么最终的期约就是拒绝状态,返回值是第一个拒绝期约的理由

Promise.race()

返回一组期约中最先解决期约的镜像。

 let p = Promise.race([
Promise.resolve(3),
new Promise((resolve, reject)=> setTimeout(reject, 1000))]);
setTimeout(console.log, 0, p);//Promise<resolved>: 3

期约扩展

期约取消

期约是内部封闭的,我们无法决定它是什么时候停止。但是也有一些方法突破这个障碍

class Cancel
{
constructor(cancelfn)
{
this.promise = new Promise((resolve, reject)=>{
cancelfn(()=>{
setTimeout(console.log, 0, 'delay cancel');
resolve();
});
});
}
}

const startButton = document.querySelector('#start');
const cancelButton =document.querySelector('#cancel');

function cancellabelDelayedResolve(delay)
{
setTimeout(console.log, 0, 'set delay');
return new Promise((resolve, reject)=>{
const id = setTimeout(()=>{
setTimeout(console.log, 0, 'delayed resolve');
resolve();
)), delay);
const cancel = new Cancel((cancelCallback)=>
cancelButton.addEventListener('click', cancelCallback));
cancel.promise.then(()=>clearTimeout(id));
});
}

期约进度通知

基本思想是首先添加notify函数,然后在合适的时刻执行。

class extendPromise extends Promise
{
constructor(executor)
{
const notifyHandlers = [];
super((resolve, reject)=>{
return executor(resolve, reject, (status)=>{
notifyHandlers.map((handler)=>handler(status));//执行每一个notify
});
});
}

notify(notifyHandler)//添加notify函数
{
this.notifyHandlers.push(notifyHandler);
}
}

let p = new extendPromise((resolve, reject, notify)=>{
function countdown(x)
{
if(x>0)
{
notify(`${20 * x}& remaining`);//对每一个notify函数传该字符串
setTimeout(()=>countdown(x - 1), 1000);
}
else
{
resolve();
}
}
countdown(5);
});

异步函数

格式:

async function foo()
{
...
}

let bar = async function(){...};
let baz = async ()=>();
class Qux
{
async qux(){...}
}

他和普通函数的区别是如果它使用return返回了值那么返回值将会由Promise.resolve()包装成期约。

async function foo()
{
return 3;
}

foo().then(console.log);
console.log(2);
//1
//2
//3(延迟执行

如果抛出错误会返回拒绝的期约

async function foo()
{
console.log(1);
throw 3;
}
foo.catch(console.log);
console.log(2);

但是如果在函数内部出现拒绝期约,这个错误不会被异步函数捕获
async function foo()
{
console.log(1);
Promise.reject(3);
}
foo.catch(conosole.log);
console.log(2);
//Uncaught(in promise): 3

await

await表示该代码要在期约结束后再执行。例如

async function baz()
{
await new Promise((resolve, reject)=> setTimeout(resolve, 1000));
console.log('baz');
}
baz();//1秒后输出

如果没有await,会先输出baz

await必须在异步函数中使用,如果不是会抛出SyntaxError异常。

使用await后执行顺序较Promise更为复杂,看一个例子

async function foo()
{
console.log(2);
console.log(await Promise.resolve(8));
console.log(9);
}

async function bar()
{
console.log(4);
console.log(await 6);
console.log(7);
}

console.log(1);
foo();
console.log(3);
bar();
console.log(5);
输出顺序是字母顺序

  1. 首先带有await的都被推迟到同步代码的末尾执行,因此先输出1、2、3、4、5
  2. 然后foo中的await部分开始执行,它会先执行期约处理程序,然后把结果放到消息队列的末尾
  3. 然后执行第二个await,因为它已经获得值6了,因此它可以继续向下执行,输出6,7
  4. 现在第一个await获得了值,输出8,9