js异步编程
期约
基础
期约类似于事件处理。例如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')); |
异步的执行器是同步执行的,所以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');//直接抛出解决的期约 |
他直接运行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(); |
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 |
异步函数
格式:async function foo()
{
...
}
let bar = async function() ;
let baz = async ()=>();
class Qux
{
async qux()
}
他和普通函数的区别是如果它使用return返回了值那么返回值将会由Promise.resolve()包装成期约。
async function foo() |
如果抛出错误会返回拒绝的期约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);
输出顺序是字母顺序
- 首先带有await的都被推迟到同步代码的末尾执行,因此先输出1、2、3、4、5
- 然后foo中的await部分开始执行,它会先执行期约处理程序,然后把结果放到消息队列的末尾
- 然后执行第二个await,因为它已经获得值6了,因此它可以继续向下执行,输出6,7
- 现在第一个await获得了值,输出8,9