[JavaScript基础] 六、期约

吐槽君 分类:javascript

1、异步编程

同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。

异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。

异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。

2、期约

ECMAScript 6新增的引用类型Promise,可以通过new操作符来实例化。创建新期约时需要传入执行器(executor)函数作为参数

2.1 期约基础

状态机

期约是一个有状态的对象,可能处于如下3种状态之一:

  • 待定(pending

  • 兑现(fulfilled,有时候也称为“解决”,resolved

  • 拒绝(rejected

待定(pending)是期约的最初始状态。在待定状态下,期约可以落定为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态

解决值、拒绝理由及期约用例

每个期约只要状态切换为兑现,就会有一个私有的内部值(value)。类似地,每个期约只要状态切换为拒绝,就会有一个私有的内部理由(reason)。在期约到达某个落定状态时执行的异步代码始终会收到这个值或理由。

通过执行函数控制期约状态

期约的状态是私有的,内部操作在期约的执行器函数中完成。

  • 控制期约状态的转换是通过调用它的两个函数参数实现的(resolve()reject()
  • 期约状态转换只能发生一次,不可撤销

Promise.resolve()

调用Promise.resolve()静态方法,可以实例化一个解决的期约。

// 这两种写法是等价的
let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();
 

给Promise.resolve()的第一个参数对应着解决的期约,使用这个静态方法,可以把任何值都转换为一个期约

setTimeout(console.log, 0, Promise.resolve());
// Promise <resolved>: undefined
setTimeout(console.log, 0, Promise.resolve(3));
// Promise <resolved>: 3
// 多余的参数会忽略
setTimeout(console.log, 0, Promise.resolve(4, 5, 6));
// Promise <resolved>: 4
 

PS:传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此,Promise.resolve()可以说是一个幂等方法

Promise.reject()

Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误(这个错误不能通过try/catch捕获,而只能通过拒绝处理程序捕获)。

2.2 期约的实例方法

Promise.prototype.then()

这个then()方法接收最多两个参数:onResolved处理程序和onRejected处理程序。

这两个处理程序参数都是可选的。而且,传给then()的任何非函数类型的参数都会被静默忽略。如果想只提供onRejected参数,那就要在onResolved参数的位置上传入undefined。

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'));
p2.then(() => onResolved('p2'),
 	    () => onRejected('p2'));

//(3秒后)
// p1 resolved
// p2 rejected

// 非函数处理程序会被静默忽略,不推荐
p1.then('gobbeltygook');
// 不传onResolved处理程序的规范写法
p2.then(null, () => onRejected('p2'));

 

Promise.prototype.then()方法返回一个新的期约实例。这个新期约实例基于onResovled处理程序的返回值构建。

Promise.prototype.catch()

Promise.prototype.catch()方法用于给期约添加拒绝处理程序。

这个方法只接收一个参数:onRejected处理程序

事实上,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype.then(null, onRejected)。

Promise.prototype.finally()

Promise.prototype.finally()方法用于给期约添加onFinally处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。

但onFinally处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码

非重入期约方法

当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。这个特性被称为非重入特性。

非重入适用于onResolved/onRejected处理程序、catch()处理程序和finally()处理程序。

传递解决值和拒绝理由

在执行函数中**,解决的值和拒绝的理由是分别作为resolve()和reject()的第一个参数往后传的**。然后,这些值又会传给它们各自的处理程序,作为onResolved或onRejected处理程序的唯一参数

let p1 = new Promise((resolve, reject) => resolve('foo'));
p1.then((value) => console.log(value)); // foo
let p2 = new Promise((resolve, reject) => reject('bar'));
p2.catch((reason) => console.log(reason)); // bar
 

拒绝期约与拒绝错误处理

拒绝期约类似于throw()表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理。

2.3 期约连锁与期约合成

期约连锁

把期约逐个地串联起来是一种非常有用的编程模式。

因为每个期约实例的方法(then()、catch()和finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的**“期约连锁”**。

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秒后)
 

Promise.all()和Promise.race()

Promise类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all()Promise.race()

Promise.all()

Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象(一般用期约对象),返回一个新期约。

如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序:

// promise 语法
let p = Promise.all([
 Promise.resolve(3),
 Promise.resolve(),
 Promise.resolve(4)
]);
// 合成期约的解决值就是所有包含期约解决值的数组
p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4]
 

Promise.all()静态方法,如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的期约也会拒绝:

// 永远待定
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise <pending>

// 一次拒绝会导致最终期约拒绝
let p2 = Promise.all([
 Promise.resolve(),
 Promise.reject(),
 Promise.resolve()
]);
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught (in promise) undefined
 

Promise.race()

Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像

Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约:

// 解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([
 Promise.resolve(3),
 new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
setTimeout(console.log, 0, p1); // Promise <resolved>: 3
 

3、异步函数

异步函数,也称为“async/await”(语法关键字),是ES6期约模式在ECMAScript函数中的应用。async/await旨在解决利用异步结构组织代码的问题。

3.1 async

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

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

异步函数如果使用return关键字返回了值(如果没有return则会返回undefined),这个值会被*Promise.resolve()*包装成一个期约对象。

async function foo() {
 return 1;
}
// 给返回的期约添加一个解决处理程序
foo().then(console.log); 
//1
 

在异步函数中抛出错误会返回拒绝的期约:(拒绝期约的错误不会被异步函数捕获)

async function foo() {
 console.log(1);
 throw 3;
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3
 

3.2 await

await关键字会暂停执行异步函数后面的代码,让出JavaScript运行时的执行线程。

await关键字同样是尝试“解包”对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行。

await关键字可以暂停异步函数代码的执行,等待期约解决:

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

await关键字必须在异步函数中使用,不能在顶级上下文如

3.3 停止和恢复执行

async/await中真正起作用的是await。async关键字,无论从哪方面来看,都不过是一个标识符。

await关键字并非只是等待一个值可用那么简单。JavaScript运行时在碰到await关键字时,会记录在哪里暂停执行。等到await右边的值可用了,JavaScript运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。

当await后面跟着一个立即可用的值,函数的其余部分也会被异步求值。下面的例子演示了这一点:

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

如果await后面是一个期约,则问题会稍微复杂一些。此时,为了执行异步函数,实际上会有两个任务被添加到消息队列并被异步求值

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
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9

 

运行时会像这样执行上面的例子:

(1) 打印1;
(2) 调用异步函数foo();
(3)(在foo()中)打印2;
(4)(在foo()中)await关键字暂停执行,向消息队列中添加一个期约在落定之后执行的任务;
(5) 期约立即落定,把给await提供值的任务添加到消息队列;
(6) foo()退出;
(7) 打印3;
(8) 调用异步函数bar();
(9)(在bar()中)打印4;
(10)(在bar()中)await关键字暂停执行,为立即可用的值6向消息队列中添加一个任务;
(11) bar()退出;
(12) 打印5;
(13) 顶级线程执行完毕;
(14) JavaScript运行时从消息队列中取出解决await期约的处理程序,并将解决的值8提供给它;
(15) JavaScript运行时向消息队列中添加一个恢复执行foo()函数的任务;
(16) JavaScript运行时从消息队列中取出恢复执行bar()的任务及值6;
(17)(在bar()中)恢复执行,await取得值6;
(18)(在bar()中)打印6;
(19)(在bar()中)打印7;
(20) bar()返;
(21) 异步任务完成,JavaScript从消息队列中取出恢复执行foo()的任务及值8;
(22)(在foo()中)打印8;
(23)(在foo()中)打印9;
(24) foo()返回。

3.4 异步函数策略

串行执行期约

使用async/await,期约连锁会变得很简单:

async function addTwo(x) {return x + 2;}
async function addThree(x) {return x + 3;}
async function addFive(x) {return x + 5;}
async function addTen(x) {
 for (const fn of [addTwo, addThree, addFive]) {
 x = await fn(x);
 }
 return x;
}
addTen(9).then(console.log); // 19
 

回复

我来回复
  • 暂无回复内容