同步与异步

同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。这样的执行流程容易分析 程序在执行到代码任意位置时的状态(比如变量的值)。

异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问 一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。 异步操作的例子可以是在定时回调中执行一次简单的数学计算:

1
2
3
 let x = 3;  

setTimeout(() => x = x + 4, 1000);

以往的异步编程模式

在早期的 JavaScript 中,只支持定义回调函数来表明异步操作完成。串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(俗称“回调地狱”)来解决。

1.异步返回值

2.失败处理

3.嵌套异步回调

如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。随着代码越来越复杂,回调策略是不具有扩展性的。“回调地狱”这个称呼可谓名至实归。 嵌套回调的代码维护起来就是噩梦

期约(promise)

期约基础

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

下面的例子使用了一个空函数对象来应付一下解释器:

1
2
3
let p = new Promise(() => {}); 

setTimeout(console.log, 0, p); // Promise <pending>

之所以说是应付解释器,是因为如果不提供执行器函数,就会抛出 SyntaxError。

期约状态机

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

待定(pending)

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

拒绝(rejected)

1.在待定状态下,期约可以落定为代表成功的兑现 (fulfilled)状态,或者代表失败的拒绝(rejected)状态。无论落定为哪种状态都是不可逆的。

2.不能保证期约必然会脱离待定状态。

3.要组织合理的代码,无论期约解决(resolve)还是拒绝(reject),甚至永远处于待定(pending)状态,都应该具有恰当的行为。

期约两大用途

1.抽象地表示一个异步操作。

期约的状态代表期约是否完成。“待定” 表示尚未开始或者正在执行中。“兑现”表示已经成功完成,而“拒绝”则表示没有成功完成。

假设期约要向服务器发送一个 HTTP 请求。请求返回 200-299 范围内的状态码就足以让期约的状态变为“兑现”。类似地,如果请求返回的状态码不在 200-299 这个范围内, 那么就会把期约状态切换为“拒绝“。

2.期约封装的异步操作会实际生成某个值,而程序期待期约状态改变时可以访问这个值。

假设期约向服务器发送一个 HTTP 请求并预定会返回一个 JSON。如果请求返回范围在 200-299 的状态码,则足以让期约的状态变为兑现。此时期约内部就可以收到一个 JSON 字符串。类似地,如果请求返回的状态码不在 200-299 这个范围内,那么就会把期约状态切换为拒绝。此时拒绝的理由可能是一个 Error 对象,包含着 HTTP 状态码及相关错误消息

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

1.期约的状态是私有的,所以只能在内部进行操作

2.执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。

3.控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为 resolve()和 reject()。调用 resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。另外,调用 reject()也会抛出错误。

Promise.resolve()

Promise.resolve() 期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用 Promise.resolve()静态方法,可以实例化一个解决的期约。

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

1
setTimeout(console.log, 0, Promise.resolve());  // Promise : undefined  setTimeout(console.log, 0, Promise.resolve(3)); // Promise : 3

对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此, Promise.resolve()可以说是一个幂等方法

1
2
3
4
5
let p = Promise.resolve(7); 
setTimeout(console.log, 0, p === Promise.resolve(p));
// true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p)));
// true

这个幂等性会保留传入期约的状态。

Promise.reject()

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

下面的两个期约实例实际上是一样的:

1
2
3
let p1 = new Promise((resolve, reject) => reject());  

let p2 = Promise.reject();

关键在于,Promise.reject()并没有照搬 Promise.resolve()的幂等逻辑。如果给它传一个期 约对象,则这个期约会成为它返回的拒绝期约的理由:

1
2
3
4
setTimeout(console.log, 0, Promise.reject(Promise.resolve())); 

// Promise : Promise

同步/异步执行的二元性

两种模式下抛出错误的情形:

1
2
3
4
5
6
7
8
9
10
11
try { 
throw new Error('foo');
} catch(e) {
console.log(e); // Error: foo
}
try {
Promise.reject(new Error('bar'));
} catch(e) {
console.log(e);
}
// Uncaught (in promise) Error: bar

1.第一个 try/catch 抛出并捕获了错误,第二个 try/catch 抛出错误却没有捕获到。

2.这里的同步代码之所以没有捕获期约抛出的错误,是因为它没有通过异步模式捕获错误。从这 里就可以看出期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式 的媒介。

3.在前面的例子中,拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队 列来处理的。因此,try/catch 块并不能捕获该错误。代码一旦开始以异步模式执行,则唯一与之交互 的方式就是使用异步结构——更具体地说,就是期约的方法。

期约的实例方法

实现 Thenable 接口

在 ECMAScript 暴露的异步结构中,任何对象都有一个 then()方法。这个方法被认为实现了 Thenable 接口。

ECMAScript 的 Promise 类型实现了 Thenable 接口。

Promise.prototype.then()

Promise.prototype.then()是为期约实例添加处理程序的主要方法。这个 then()方法接收最多 两个参数:onResolved 处理程序和 onRejected 处理程序。这两个参数都是可选的,如果提供的话, 则会在期约分别进入“兑现”和“拒绝”状态时执行。传给then()的任何非函数类型的参数都会被静默忽略。

Promise.prototype.then()方法返回一个新的期约实例:
1
2
3
4
5
let p1 = new Promise(() => {}); 
let p2 = p1.then();
setTimeout(console.log, 0, p1); // Promise <pending>
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false

这个新期约实例基于 onResovled 处理程序的返回值构建。换句话说,该处理程序的返回值会通过 Promise.resolve()包装来生成新期约。如果没有提供这个处理程序,则 Promise.resolve()就会 包装上一个期约解决之后的值。如果没有显式的返回语句,则 Promise.resolve()会包装默认的返回值 undefined

onRejected 处理程序也与之类似:onRejected 处理程序返回的值也会被 Promise.resolve() 包装。乍一看这可能有点违反直觉,但是想一想,onRejected 处理程序的任务不就是捕获异步错误吗? 因此,拒绝处理程序在捕获错误后不抛出异常是符合期约的行为,应该返回一个解决期约。

Promise.prototype.catch()

Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数: onRejected 处理程序。

传递解决值和拒绝理由

到了落定状态后,期约会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理 程序。拿到返回值后,就可以进一步对这个值进行操作。比如,第一次网络请求返回的 JSON 是发送第 二次请求必需的数据,那么第一次请求返回的值就应该传给 onResolved 处理程序继续处理。当然,失 败的网络请求也应该把 HTTP 状态码传给 onRejected 处理程序。

1.在执行函数中,解决的值和拒绝的理由是分别作为 resolve()和 reject()的第一个参数往后传 的。

2.这些值又会传给它们各自的处理程序,作为 onResolved 或 onRejected 处理程序的唯一 参数。

1
2
3
4
let p1 = new Promise((resolve, reject) => resolve('foo')); 
p1.then((value) => console.log(value)); // foo then里面只有一个onresolved参数,所以就是onResolved 处理程序
let p2 = new Promise((resolve, reject) => reject('bar'));
p2.catch((reason) => console.log(reason)); // bar catch

Promise.resolve()和 Promise.reject()在被调用时就会接收解决值和拒绝理由。同样地,它 们返回的期约也会像执行器一样把这些值传给 onResolved 或 onRejected 处理程序:

1
2
3
4
let p1 = Promise.resolve('foo'); 
p1.then((value) => console.log(value)); // foo
let p2 = Promise.reject('bar');
p2.catch((reason) => console.log(reason)); // bar

拒绝期约与拒绝错误处理

1.拒绝期约类似于 throw()表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理。在期约的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。

2.期约可以以任何理由拒绝,包括 undefined,但最好统一使用错误对象。

期约连锁与期约合成

期约连锁

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

1
2
3
4
5
6
7
8
9
10
11
let p = new Promise((resolve, reject) => { 
console.log('first');
resolve();
});
p.then(() => console.log('second'))
.then(() => console.log('third'))
.then(() => console.log('fourth'));
// first
// second
// third
// fourth

这个实现最终执行了一连串同步任务。没有那么有用。使用 4 个同步函数也可以做到:

1
2
3
4
(() => console.log('first'))(); 
(() => console.log('second'))();
(() => console.log('third'))();
(() => console.log('fourth'))();

要真正执行异步任务,可以改写前面的例子,让每个执行器都返回一个期约实例。

这样就可以让每 个后续期约都等待之前的期约,也就是串行化异步任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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()

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

2.如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的 期约也会拒绝:

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

4.如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由。之后再拒绝的期 约不会影响最终期约的拒绝理由。

Promise.race()

1.Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个 方法接收一个可迭代对象,返回一个新期约:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([
Promise.resolve(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
setTimeout(console.log, 0, p1); // Promise <resolved>: 3
// 拒绝先发生,超时后的解决被忽略
let p2 = Promise.race([
Promise.reject(4),
new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);
setTimeout(console.log, 0, p2); // Promise <rejected>: 4
// 迭代顺序决定了落定顺序
let p3 = Promise.race([
Promise.resolve(5),
Promise.resolve(6),
Promise.resolve(7)
]);
setTimeout(console.log, 0, p3); // Promise <resolved>: 5

3.如果有一个期约拒绝,只要它是第一个落定的,就会成为拒绝合成期约的理由。之后再拒绝的期约 不会影响最终期约的拒绝理由。

promise的异常处理(附加)

image-20231107221319535

promise异常会向着链条向后传递

catch能捕获链式上的上一个promise对象的异常

image-20231107221319535

image-20231107221319535

如果使用then,只能捕获ajax这个promise对象的异常

image-20231107221319535

image-20231107221319535

可以注册一个全局处理异常对象事件在window上

image-20231107221319535

而在node当中,需要在process当中注册这个事件

image-20231107221319535

不过还是尽量在代码中明确捕获每一个异常,而不是丢给全局。