Promise与异步函数(async)

一、什么是Promise

Promise,中文译为期约(期待与约定),是一个标识异步操作结果的对象。通过Promise可以观察异步操作的结果。Promise对象不是结果,而是结果的占位符。Promise本身并不执行任何操作,它只是一种观察异步操作结果的方式。

二、我们用Promise来解决什么问题

在Promise出现之前,JavaScript在解决异步编程时通常使用回调函数(callback)来观察获取异步操作的结果,callback机制存在了一些显而易见的问题:

  • 当完成一个业务需要多个异步操作时,会产生深度回调嵌套,就是所谓的回调地狱
  • 回调函数没有标准的成功/失败后报错的方式,每个函数或者API都需要开发者自定义
  • 没有标准的指示成功/失败的方案意味着没有通用的工具来管理复杂度

Promise就是试图通过提供简单的、标准化的语法来处理使用回调时遇到的问题。

三、Promise基本概念

Promise对象具有三种可能的状态

  1. 待定 pending    Promise对象正在进行中
  2. 兑现 fullfilled    Promise对象已完成(决议)并有一个值,这通常意味着成功
  3. 拒绝 rejected   Promise对象已完成(决议)并由一个拒绝的原因,这通常意味着失败

Promise对象一般以待定状态作为初始状态(这取决于Promise对象的创建方式,并不是所有的Promise对象初始状态都是待定),与待定状态相对的是敲定状态(settled),无论是对象状态或是拒绝状态都属于敲定状态。

待定状态的期约状态可能会发生改变,变为敲定状态。而一旦期约的状态进入敲定状态,是不可能再变回待定状态的。

期约的状态是私有的,不能通过JavaScript检测或修改。

当期约切换为兑现状态,可以有一个私有的内部值,而拒绝状态的期约可以有一个拒绝的理由(也被称为错误)。值或者拒绝理由取决于期约的设计者,两者的默认值都是undefined,当期约达到某个落地状态时执行的异步代码会收到这个值或理由。

四、创建Promise

创建一个期约对象由多种方式,可以使用构造器也可以使用Promise的静态方法。

构造器与执行器函数

使用构造器创建期约对象时,必须传入一个执行器函数作为参数。

执行器函数的作用是:

  • 初始化期约的异步行为
  • 控制期约的状态转化
let p = new Promise((resolve,reject)=>setTimeout(resolve,1000));

p就是一个通过构造器创建出来的期约对象,其初始状态为待定。 

随着期约对象的创建,执行器函数马上开始执行

执行器函数包含两个参数,通常被命名为resolve和reject,是两个函数。通过调用这两个函数就可以实现期约状态的转变。调用resolve函数,期约的状态就转为兑现,而调用reject函数,期约的状态就转为拒绝。另外,调用reject时还会抛出错误。

所以期约对象在大概1000ms之后,状态就会转变为兑现。

无论resolve()还是reject(),状态发生转换就不可撤销了。于是继续修改状态的代码会被自动忽视:

let p = new Promise((resolve,reject)=>{
    resolve();
    reject(); //代码被自动忽视没有效果
});

静态方法

期约并不一定初始状态就是待定,要经过执行器函数才能转变状态。通过Promise的静态方法创建出的期约对象就可以立刻拥有敲定的某个状态。

resolve

利用Promise.resolve()创建的期约对象就是兑现状态。

let p1 = new Promise((resolve,reject)=>resolve());
let p2 = Promise.resolve();

上面两句代码的含义是一样的, 创建出来的p1和p2都是兑现状态。

前面说过,兑现状态的期约是可以有一个值的。这个值可以通过resolve函数的第一个参数传入,例如:

setTimeout(console.log,0,Promise.resolve());// Promise<resolved>:undefined
setTimeout(console.log,0,Promise.resolve(3));//Promise<resolved>:3

 这个resolve函数可以把任何传入的内容包装成一个期约对象,即使你传入一个Error对象,或者一个期约对象。

setTimeout(console.log,0,Promise.resolve(new Error("foo")))

兑现的期约的值是一个Error对象,会导致在控制台输出错误相关信息

如果为resolve传入另一个期约对象的话,resolve的行为就好像变成了“透明”的,会保持传入的期约的完整性并且不会再额外的套上一层期约的“外壳”:

let p = Promise.resolve(1024);
setTimeout(console.log,0,p);//Promise<resovled>:1024
setTimeout(console.log,0,p===Promise.resolve(p));//true

 因此可以说Promise.resolve是一个幂等的方法

reject

Promise还有一个可以创建期约对象的静态方法就是reject。与resolve方法略有不同的是,reject除了可以实例化期约对象并通过参数提供拒绝理由之外还会抛出一个异步错误。因为是异步错误,这个错误是不能通过try...catch...捕获到的,只能通过后面提及的拒绝处理程序捕获。

let p1 = new Promise((resovel,reject)=>reject());
let p2 = Promise.reject();

 上面两句代码的含义是一样的, 创建出来的p1和p2都是拒绝状态。

拒绝期约的理由也就是传递给reject函数的第一个参数:

let p = Promise.rejct(3);
setTimeout(console.log,0,p);//Promise<rejected>:3

 reject不具备期约的幂等性,如果给reject传入一个期约,那么这个期约会成为创建出来的拒绝期约的理由:

setTimeout(console.log,0,Promise.reject(Promise.resolve()));//Promise<rejected>:Promise<resolved>

五、Promise的实例方法

期约实例方法是连接外部同步代码与内部异步代码之间的桥梁。这些方法可以访问异步操作返回的数据(resolve的值或reject的理由与抛出的异常),连续对期约求值等。

then方法

通过then方法为期约对象添加处理函数。

then方法接受最多两个参数:分别对应期约对象进入兑现状态时执行的onResolved处理程序和期约对象进入拒绝状态是执行的onRejected处理程序。这两个参数均是可选的。

function onResolved(id){
    setTimeout(console.log,0,id,'resolved...')
}

function onRejected(id){
    setTimeout(console.log,0,id,'rejected...')
}

let p1 = new Promise((resolve,reject)=>setTimeout(resolve,3000));
let p2 = new Promise((resolve,reject)=>setTimeout(reject,3000));

p1.then(()=>onResolved('p1'),()=>onRejected('p1'));//约3秒后,打印p1 resolved...
p2.then(()=>onResolved('p2'),()=>onRejected('p2'));//约3秒后,打印p2 rejected...

 then的两个参数都是可选的。并且传递给then的任何非函数参数都会被静默忽略。

function onResolved(id){
    setTimeout(console.log,0,id,'resolved...')
}

function onRejected(id){
    setTimeout(console.log,0,id,'rejected...')
}

let p1 = new Promise((resolve,reject)=>setTimeout(resolve,3000));
let p2 = new Promise((resolve,reject)=>setTimeout(reject,3000));

p1.then(()=>onResolved('p1'));
p2.then(null,()=>onRejected('p2'));

修改后的p1只有onResovled函数,而p2只有onRejected函数。

then方法会产生一个返回值,返回值是一个新的期约对象。

let p1 =  new Promise(()=>{});
let p2 = p1.then();
setTimeout(console.log,0,p1===p2); //false

p1是状态为待定的期约对象,所以p1调用then的时候,即不会触发onResovled处理程序也不会触发onRejected处理程序,此时获得的p2也是一个待定状态的期约对象。 

如果是兑现状态的p1呢?此时p1调用then方法时会触发onResolved处理程序,onResolved处理程序的返回值通过Promise.resolve的包装生成新的期约对象p2。

then里面有没有提供onResolved处理程序,Promise.resolve直接包装上一个期约兑现时的值,也就是p2与p1的值一样,状态为兑现;如果提供了onResolved处理程序,onResolved程序返回什么值,p2的值就是什么,如果onResolved没有显式的return语句,则p2的值就是undefined。

let p1 = Promise.resolve("foo");
setTimeout(console.log,0,p1.then());//Promise<resolved>:foo
setTimeout(console.log,0,p1.then(()=>undefined));//Promise<resolved>:undefined
setTimeout(console.log,0,p1.then(()=>{}));//Promise<resolved>:undefined
setTimeout(console.log,0,p1.then(()=>Promise.resolve()));//Promise<resolved>:undefined
setTimeout(console.log,0,p1.then(()=>'bar'));//Promise<resolved>:bar
setTimeout(console.log,0,p1.then(()=>Promise.resolve("baz")));//Promise<resolved>:baz
setTimeout(console.log,0,p1.then(()=>new Promise(()=>{})));//Promise<pending>:undefined
setTimeout(console.log,0,p1.then(()=>Promise.reject()));//Promise<rejected>:undefined
setTimeout(console.log,0,p1.then(()=>{throw "baz"}));//Promise<rejected>:baz

 比较有意思的是

 setTimeout(console.log,0,p1.then(()=>{throw "baz"}));//Promise<rejected>:baz

在onResolved处理程序中抛出异常,会产生一个拒绝状态的期约对象,而拒绝的理由就是抛出的错误。 

如果是拒绝状态的p1呢?p1调用then函数会触发里面的onRejected处理程序。需要注意的是,onRejected处理程序的返回值依然是通过Promise.resolve包装生成新的期约对象p2

let p1 = Promise.reject("foo");
setTimeout(console.log,0,p1.then());//Promise<resolved>:foo
setTimeout(console.log,0,p1.then(null,()=>undefined));//Promise<resolved>:undefined
setTimeout(console.log,0,p1.then(null,()=>{}));//Promise<resolved>:undefined
setTimeout(console.log,0,p1.then(null,()=>Promise.resolve()));//Promise<resolved>:undefined
setTimeout(console.log,0,p1.then(null,()=>'bar'));//Promise<resolved>:bar
setTimeout(console.log,0,p1.then(null,()=>Promise.resolve("baz")));//Promise<resolved>:baz
setTimeout(console.log,0,p1.then(null,()=>new Promise(()=>{})));//Promise<pending>:undefined
setTimeout(console.log,0,p1.then(null,()=>Promise.reject()));//Promise<rejected>:undefined
setTimeout(console.log,0,p1.then(null,()=>{throw "baz"}));//Promise<rejected>:baz

所以会发现,以上代码的执行结果与之前是完全一样的。

catch方法

catch方法是then方法的语法糖,相当于then(null,onReject)。

let p = Promise.reject();
let onRejected = ()=>setTimeout(console.log,0,'rejected');
let p2 = p.then(null,onRejected);
let p3 = p.catch(onRejected);
setTimeout(console.log,0,p2===p3);//false

 catch既然是then的语法糖,所以catch方法自然也会返回一个新的期约对象。其工作逻辑与then(null,onRejected)完全一样。

finally方法

finally方法用来给期约对象添加onFinally处理程序。onFinally处理程序无论在期约转为兑现还是拒绝状态时都会被执行。但是onFinally处理程序本身无法预判期约最终会转为哪种状态,所以这个处理程序不宜添加针对某一种状态的代码逻辑,更多的是清理、回收资源的代码。

finally方法一样会产生一个新的期约,与then或catch方法产生新期约对象的逻辑略有不同,大多数情况下finally产生的新期约对象都是父期约对象的传递。

let p1 = Promise.resolve("foo");
setTimeout(console.log,0,p1.finally());//Promise<resolved>:foo
setTimeout(console.log,0,p1.finally(()=>undefined));//Promise<resolved>:foo
setTimeout(console.log,0,p1.finally(()=>{}));//Promise<resolved>:foo
setTimeout(console.log,0,p1.finally(()=>Promise.resolve()));//Promise<resolved>:foo
setTimeout(console.log,0,p1.finally(()=>'bar'));//Promise<resolved>:foo
setTimeout(console.log,0,p1.finally(()=>Promise.resolve("baz")));//Promise<resolved>:foo
setTimeout(console.log,0,p1.finally(()=>new Promise(()=>{})));//Promise<pending>:undefined
setTimeout(console.log,0,p1.finally(()=>Promise.reject()));//Promise<rejected>:undefined
setTimeout(console.log,0,p1.finally(()=>{throw "baz"}));//Promise<rejected>:baz

可以看到除了onFinally处理程序中返回的是待定约期对象,拒绝状态的期约对象或者onFinally处理程序中抛出了异常之外,其余的情况均与p1保持一致。 

六、一些细节

先回顾一下JavaScript的事件总线机制。主线程总是在执行调用栈中的代码,如果这些代码中有异步API(例如setTimeout),那么异步API也会向同步代码一样执行,只不过瞬间就会执行完毕,真正需要执行的内容(例如setTimeout中的func)会以任务的形式入队列排队等待。待主线程将调用栈中所有代码执行完毕后,事件总线开始检查队列中的任务。处理任务时会触发新的函数调用(例如setTimeout中的func),新的函数自然会进入执行栈中执行。当执行栈再次为空时,继续通过事件总线检查是否有下一个待处理的任务。这个处理机制会一直循环往复下去,直到调用栈为空,任务队列为空。 

期约方法非重入

如图所示,当期约进入敲定状态,与该状态相关的处理程序(onResolved或onRejected)会进入队列开始排队(而非立即执行)。这就意味着跟在添加处理程序代码之后的其它同步代码一定会在处理程序执行之前获得执行(因为这些代码在执行栈中)。

这种运行进制由JavaScript运行时(JavaScript Runtime)保证,被称为非重入机制。

let p  = Promise.resolve();
p.then(()=>console.log("onResolved handler"));
console.log("I am after then()...")

// I am after then()...
// onResolved handler

JavaScript一定会优先执行执行栈中的三行同步代码。第二行代码执行的时候,将onResolved的处理程序进行了排队。随着console.log打印I am after then()之后,执行栈空了,通过事件总线检查是否有任务需要执行时,发现了onResolved执行程序并入栈开始执行,输出onResolved handler。执行栈空,也没有新的任务出现,程序执行完毕。

再看一个例子:

let myFunc;
let p  = new Promise((resolve)=>{
    myFunc = ()=>{
        console.log("1: line before resolve");
        resolve();
        console.log("2: line after resolve");
    }
});
p.then(()=>console.log("4: onResolved handler"));
myFunc();
console.log("3: the live after myFunc");

// 1: line before resolve
// 2: line after resolve
// 3: the live after myFunc
// 4: onResolved handler

 可以看到,即使改变期约状态的myFunc执行顺序位于p.then之后,但是执行顺序逻辑并没有改变。

非重入机制也适用于onRejected、onFinally处理程序,这些处理程序只能“异步”的执行。

如果给期约添加了多个处理程序,当期约状态发生变化时,相关处理程序会按照它们的添加顺序进行执行:

let p1 = Promise.resolve();
let p2 = Promise.reject();
p1.then(()=>setTimeout(console.log,0,1));
p1.then(()=>setTimeout(console.log,0,2));
p2.then(null,()=>setTimeout(console.log,0,'a'));
p2.then(null,()=>setTimeout(console.log,0,'b'));
p1.finally(()=>setTimeout(console.log,0,3))
p1.finally(()=>setTimeout(console.log,0,4))
p2.catch(()=>setTimeout(console.log,0,'c'));
p2.catch(()=>setTimeout(console.log,0,'d'));
// 1
// 2 
// a
// b
// 3
// 4
// c
// d

 这个程序如果仅仅生成一个拒绝的期约p2,但是没有添加onRejected处理程序(也就是注释掉所有p2.then和p2.cache代码),程序执行时会显示一个非捕获的错误!所以拒绝期约类似于throw表达式,代表了一种异常的程序状态,是应该需要中断程序或者特殊处理的。throw应该被try...catch...,拒绝的期约应该被onRejected。而onRejected之于拒绝期约的意义应该与catch之于throw的意义一样,修正错误,让程序可以继续执行。

 传递解决值和拒绝理由

在执行函数中,resolve函数和reject函数的第一个参数分别就是值和拒绝理由,向后传递给onResolved或onRejected处理程序。onResolved和onRejected可以继续生成新的期约对象继续向后传递。

function myFunc(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            const succeed = Math.random()<0.5;
            if(succeed){
                console.log("resolving...");
                resolve(42);
            }else{
                console.log("rejecting...");
                reject("failed");
            }

        },100);
    });
}

myFunc().then(val=>console.log("fulfilled with",val))
         .catch(err=>console.log("rejected with",err))
         .finally(()=>{console.log("finally!");});

拒绝期约的错误处理

一个拒绝的期约类似于throw表达式,代表了一种异常的程序状态,是应该需要中断程序或者特殊处理的。throw应该被try...catch...,拒绝的期约应该被onRejected。而onRejected之于拒绝期约的意义应该与catch之于throw的意义一样,修正错误,让程序可以继续执行。

let p1 = new Promise((resolve, reject) => {
    reject(Error('err1'));
})
let p2 = new Promise((resolve, reject) => {
    throw Error('err2');
})
let p3 = Promise.resolve().then(()=>{throw Error('err3');})
let p4 = Promise.reject(Error('err4'));
setTimeout(console.log, 0,p1);
setTimeout(console.log, 0,p2);
setTimeout(console.log, 0,p3);
setTimeout(console.log, 0,p4);

//4个未捕获的异常

 期约中抛出的错误都是以异步形式从消息队列抛出的,所以它无法被try...catch...,同时也不会影响执行栈中后序同步代码的执行。

throw Error('error');
console.log('foo');//不会执行
Promise.reject(Error("err"));
console.log("bar");//正常执行

就如前面演示过的,异步错误只能通过异步的onRejected处理程序捕获修复。 

try{
    Promise.reject(Error("err"));
}catch(e){
    console.log("oops!") //根本不会执行
}
//有未捕获的异常
//捕获异步的异常
Promise.reject(Error("err")).catch((e)=>{console.log("got ya")});

七、期约连锁与期约合成

将多个期约组合在一起可以构成强大的代码逻辑。组织多个期约的方式有两种:

期约连锁

把多个期约逐个串联起来。之所以可以这样做,是因为then、catch、finally都能返回一个新的期约对象。

let p = new Promise((resolve, reject) => {
    console.log("first");
    resolve();
})
p.then(()=>console.log("second"))
 .then(console.log("third"))
 .then(console.log("fourth"));

这个实现最终执行了一连串同步任务(简单的console.log)。

如果在处理器返回的是期约呢?那么就可以实现异步任务的串行执行!

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 executor");
    setTimeout(resolve,1000);
})).then(()=>new Promise((resolve, reject) => {
    console.log("p4 executor");
    setTimeout(resolve,1000);
}))
//p1 executor 约1秒后
//p2 executor 约2秒后
//p3 executor 约3秒后
//p4 executor 约4秒后

每个后序的处理都会等待前一个期约的完成(完成的标志是期约状态的改变并触发onResolved处理程序)。这种结构可以简洁的将异步任务串行化,有效的解决了依赖回调的问题。

再看一个例子,后序期约使用之前期约的兑现值来形成串行执行。如果用标准的函数来写的话,就是同步的函数嵌套:

function add2(x){return x+=2}
function add3(x){return x+=3}
function add5(x){return x+=5}
function sum(x){
    return add5(add3(add2(x)));
}
console.log(sum(10));//20

 可以用期约来实现:

function add2(x){return x+=2}
function add3(x){return x+=3}
function add5(x){return x+=5}
function sum(x){
    return Promise.resolve(x).then(add2).then(add3).then(add5);
}
sum(10).then(console.log);//20

再进一步,可以提炼成一个工厂函数:

function compose(...fns){
    return (x)=>fns.reduce((p,f)=>p.then(f),Promise.resolve(x));
}

所以上面的期约串联可以继续改写成:

function compose(...fns){
    return (x)=>fns.reduce((p,f)=>p.then(f),Promise.resolve(x));
}

compose(add2,add3,add5)(10).then(console.log);//20

期约合成

Promise还提供了将多个期约合成一个期约的两个静态方法:all方法和race方法。

all方法

all方法接受一个可迭代对象作为参数,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,"p fulfilled")});

// Promise { <pending> }
// p fulfilled 大约1秒后

如果包含至少一个待定的期约,合成期约也是待定;如果包含至少一个拒绝的期约,合成的期约也是拒绝的。

let p = Promise.all([
    Promise.resolve(),
    Promise.resolve(),
    Promise.reject()
]);
p.then(null,()=>{setTimeout(console.log,0,"p rejected")});
//p rejected

all真正强大的地方在于,如果一组期约都兑现了,那么合成期约的值是这一组期约值所组成的数组,并按照一组期约在容器中的顺序来呈现。如果一组期约中有一个拒接了,则第一个拒接期约的理由将成为合成期约拒接的理由。之后再有拒接的期约,其理由不会影响合成期约拒绝的理由。

let p = Promise.all([
    Promise.resolve(2),
    Promise.resolve(),
    Promise.resolve(4)
]);
p.then((val)=>{setTimeout(console.log,0,val)});
//[2,undefined,4]
let p = Promise.all([
    Promise.reject("foo"),
    new Promise((resolve, reject) => {
        setTimeout(reject,1000,"bar");
    })
]);
p.then(null,(err)=>{setTimeout(console.log,0,err)});
//foo

 只要合成期约添加了 onRejected处理程序,虽然一组中的两个期约都拒绝了,而且第二个拒绝的期约还不能影响合成期约拒绝的理由,但是不会有未捕获的异常。

race方法

race方法接受一个可迭代对象作为参数。race方法的返回值是一组期约中最先敲定的期约的“镜像”(状态一样,值或理由一致,但是是一个新的期约)。

let p1 = Promise.race([
    Promise.resolve(3),
    new Promise((resolve,reject)=>{setTimeout(reject,1000,"foo")})
]);

p1.then((val)=>{console.log(val)},(err)=>console.log(err));
//3
let p1 = Promise.race([
    Promise.reject("foo"),
    new Promise((resolve,reject)=>{setTimeout(resolve,1000,"bar")})
]);

p1.then((val)=>{console.log(val)},(err)=>console.log(err));
//foo

只看敲定与兑现还是拒绝无关。

如果一组多个期约是同时完成的,则按照书写顺序,最先写的期约是最终的合成期约镜像:

let p1 = Promise.race([
    Promise.reject("foo"),
    Promise.reject("bar"),
    Promise.resolve("baz"),
    
]);

p1.then((val)=>{console.log(val)},(err)=>console.log(err));
//foo

八、异步函数async

Promise是ES6的产物。在ES8中加入了异步函数的概念,也就是通过使用新增关键字async / await将期约与标准函数进行结合使用。

异步函数的出现时为了进一步解决异步代码的结构组织问题。

异步函数概览

理解异步函数其实就是理解async和await

async

async用来声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上。

async让函数具有异步特征,但其实总体上代码仍然是同步求值的。

async function myFunc(){
    console.log(1);
}

myFunc();
console.log(2);

// 1
// 2

 myFunc函数与console.log按次序执行。

async的真正奥义在于,如果异步函数使用return关键字返回内容(没有return就是返回undefined),这个值会被Promise.resolve函数包装成一个期约对象。换句话说异步函数始终返回期约对象。

async function myFunc(){
    console.log(1);
    return 3;
}

myFunc().then(console.log);
console.log(2);
// 1
// 2
// 3

不用刻意在异步函数中返回Promise对象,不要忘记前面介绍过的Promise.resolve的等幂性。

async function myFunc(){
    console.log(1);
    return Promise.resolve(3);//这与直接返回一个3效果完全一样
}

myFunc().then(console.log);
console.log(2);
// 1
// 2
// 3

 原则上(所谓原则上,其实就是实际应用中无所谓的意思),异步函数的返回值应该是一个thenable接口对象(再次强调,真的是无所谓的)。所谓的thenable接口对象就是对象中包含了一个then函数(实现了JavaScript中的thenable接口,这个接口中有一个函数是then)。如果异步函数的返回值是一个thenable对象,那么就可以使用then中提供的处理程序来处理数据。

例如:

async function myFunc(){
    //rst是一个实现了thenable接口的对象
    const rst =  {then(callback){callback("foo")}}
    return rst
}

myFunc().then(console.log);//foo

但实际上前面也演示过,返回值如果不是thenable对象,返回值就会被Promise.resolve函数处理为期约对象。

有一点需要注意的是,在异步函数中throw产生的异常onRejected处理程序可以处理,但是Promise.reject产生的异常即使onRejected处理程序都无法捕获:

async function myFunc(){
    console.log(1);
    Promise.reject(3);
}

myFunc().catch(console.log);
console.log(2);
// 1
// 2
// 未捕获的异常

await

异步函数主要针对不会马上完成的任务。而使用await可以挂起异步函数的执行,等待期约兑现(fulfilled)

async function myFunc(){
    let p = new Promise(resolve=>setTimeout(resolve,1000,3));
    console.log(await p);
}
myFunc();
//3

await关键字会挂起异步函数的执行,让出JavaScript运行时的执行线程。 await关键字可以解包期约中的内容,获得一个值(兑现的值)或者抛出一个异常(拒绝的理由),然后恢复异步函数的执行。

async如果没有await,异步函数的行为和同步函数没什么区别。但是一旦异步函数中有了await,其执行会受到很大的影响。await与yield关键字有些类似,JavaScript碰到await后,会挂起函数的执行并等待await右边的值可用。await关键字不会导致程序阻塞(要么执行栈中后序的代码,要通过事件总线检查任务)

当右边的值可以使用的时候,JavaScript运行时会向队列中推送一个任务,通过任务来恢复异步函数从挂起的地方继续执行:

async function func(){
    console.log(2);
    await null;
    console.log(4);
}
console.log(1);
func();
console.log(3);
//1
//2
//3
//4

await后面跟着一个立即可用的值(null),但是也会向消息队列中发送一个任务并中止func函数的执行!随着cosole.log(3)执行完毕,执行栈空了之后,从队列中取出任务,继续将func函数从挂起的地方继续执行后序的代码。

利用await我们可以轻松实现JavaScript版本的sleep函数

function sleep(delay){
    return new Promise((resolve, reject) => {
        setTimeout(resolve,delay);
    })
}

async function foo(){
    console.log("foo start");
    const t0 = Date.now();
    await sleep(2000);
    console.log(Date.now()-t0);
    console.log("foo end");
}

foo();

如果使用await“对付”Promise.reject是可以被onRejected所“捕获的”,但是await可以解包,因此awiat会“释放”拒绝期约中的错误,导致异步函数后面的内容无法运行了(相当于同步抛出了错误,前面有过类似的代码演示)。

async function myFunc(){
    console.log(1);
    await Promise.reject(3);
    console.log(4);//无效代码,静默忽略
}

myFunc().catch(console.log);
console.log(2);
// 1
// 2
// 3

await关键字必须写在异步函数的内部,不能在顶级上下文(例如<script>)或模块中使用。另外,异步函数的特质不会扩展到嵌套的函数,因此如果有异步函数与普通函数嵌套的情况,await关键字只能直接出现在异步函数的定义中

异步函数的使用模式

异步任务串行执行

function randomDelay(idx){
    const delay =  Math.random()*5000;
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
            console.log(idx," finished");
        }, delay);
    })
}

async function foo(){
    const t0 = Date.now();
    await randomDelay(1);
    await randomDelay(2);
    await randomDelay(3);
    await randomDelay(4);
    await randomDelay(5);
    console.log("seconds: ", (Date.now()-t0)/1000);

}

foo()

foo函数的执行周期会很长,因为要挂起5次,等着5个期约兑现后,还需要5次重任务队列继续执行foo函数后序内容。这样的foo函数把5个异步任务按任务发布顺序依次完成了。

如果异步任务无需按照书写顺序完成(只要都完成了即可),那么可以先启动异步任务,在await结果:

function randomDelay(idx){
    const delay =  Math.random()*5000;
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
            console.log(idx," finished: ",Date.now());
        }, delay);
    })
}

async function foo(){
    const t0=Date.now();
    let p1 = randomDelay(1);
    let p2 = randomDelay(2);
    let p3 = randomDelay(3);
    let p4 = randomDelay(4);
    let p5 = randomDelay(5);
    
    await p1;
    await p2;
    await p3;
    await p4;
    await p5;


    console.log("seconds: ", (Date.now()-t0)/1000);

}

foo()

这里需要注意的是,5个异步任务是在await之前几乎是同时开始的。所以这个程序执行的步骤是,await在挂起foo函数等待p1兑现的时候,p2的setTimeout延时任务已经发送到队列了,此时就会开始执行相关内容,p2兑现并且打印2 finished,然后p1的setTimeout任务才到队列,开始执行相关内容,p1兑现并且打印1 finishe,p1兑现了foo函数继续执行,然后挂起等待p2兑现,p2已经兑现了,函数继续执行,然后挂起等待p3兑现。

所以这个函数的执行效果时,异步任务执行的顺序与书写顺序是可能是不一致的,但是5次挂起,5次恢复(有些挂起和恢复执行可能是瞬间完成的)执行依然是按照顺序进行的。

  • 13
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
异步函数是指可以在函数执行过程中进行其他操作,并且不会阻塞当前线程。Swift 5.5 中引入的 async/await 机制可以更加方便地编写异步代码。async/await 是一种便捷的语法糖,可以使我们使用类似于同步代码的方式来编写异步代码。 在使用 async/await 时,我们需要将异步操作包装在一个异步函数内部,并使用 async 标记该函数。在这个函数内部,可以使用 await 关键字来等待一个异步操作的结果。使用 await 关键字来等待异步操作的结果时,该操作会在后台线程上执行,当前线程会暂时挂起,直到异步操作完成。 async/await 的运行机制是基于 Swift 的协程机制。在一个异步函数中,当遇到需要等待的异步操作时,函数会被暂停,并且当前的函数状态会被保存。然后,异步操作会在后台线程上执行,一旦完成,会将结果返回给原来的异步函数。然后异步函数会继续执行,直到遇到下一个需要等待的异步操作或者函数结束。 使用 async/await 可以使得异步代码更加易读和可维护。我们可以使用常见的流程控制语句如 if、for 和 try-catch 来编写异步代码,而不再需要使用回调函数或者 Promise 链式调用来处理异步操作。这种方式提供了更加直观和简洁的代码结构。 总结起来,async/await 是 Swift 中用于编写异步代码的一种便捷的语法糖。它基于协程机制,可以使得异步代码更加易读和可维护。通过 async/await,我们可以使用类似于同步代码的方式来编写异步代码,提高开发效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值