• JavaScript时单线程事件循环模型,同步操作与异步操作时代码依赖的核心机制。

同步与异步

  • 同步行为对应内存中顺序执行的处理器指令
  • 每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。
  • 异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行

期约基础

  • 可以通过new操作符来实例化Promise。
  • 创建期约时需要传入执行器函数作为参数。
1
2
let p = new Promise(() => {}); 
setTimeout(console.log, 0, p); // Promise <pending>

期约状态机

  • 期约是一个有状态的对象,包括下面三种状态之一:

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

  • 无论落定为哪种状态都是不可逆的。

  • 只要从待定转换为兑现或拒绝,期约的状态就不再改变。而且,也不能保证期约必然会脱离待定状态。

  • 期约的状态是私有的,不能直接通过JavaScript检测到。

  • 期约故意将异步行为封装起来,从而隔离外部的同步代码。

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

  • 期约主要有两大用途:
    • 抽象地表示一个异步操作。期约的状态代表期约是否完成。”待定”表示尚未开始或者正在执行中。“兑现”表示已经成功完成,而“拒绝”则表示没有成功完成。
    • 期约封装的异步操作会实际生成某个值,而程序期待期约状态改变时可以访问这个值。相应地,如果期约被拒绝,程序就会期待期约状态改变时可以拿到拒绝的理由。

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

  • 执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。
    • 控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为resolve()和 reject()。
    • 调用resolve()会把状态切换为兑现;
    • 调用reject ()会把状态切换为拒绝。另外,调用reject ()也会抛出错误。
1
2
3
4
5
6
7
8
let p = new Promise((resolve, reject) => { 
setTimeout(reject, 10000); // 10 秒后调用 reject()
// 执行函数的逻辑
});
setTimeout(console.log, 0, p); // Promise <pending>
setTimeout(console.log, 11000, p); // 11 秒后再检查状态
// (After 10 seconds) Uncaught error
// (After 11 seconds) Promise <rejected>

Promise.resolve()

  • 期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。
  • 通过调用Promise.resolve()静态方法,可以实例化一个解决的期约。
1
2
3
let p1 = new Promise((resolve, reject) => resolve());
//等价于
let p2 = Promise.resolve();
  • 使用这个静态方法,实际上可以把任何值都转换为一个期约。
1
2
3
4
5
6
7
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
  • 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
  • 这个静态方法能够包装任何非期约值,包括错误对象,并将其转换为解决的期约。因此,也可能导致不符合预期的行为。
1
2
3
let p = Promise.resolve(new Error('foo')); 
setTimeout(console.log, 0, p);
// Promise <resolved>: Error: foo

Promise.reject()

  • Promise.reject ()会实例化一个拒绝的期约并抛出一个异步错误(这个错误不能通过try/catch 捕获,而只能通过拒绝处理程序捕获)。
  • 拒绝的期约的理由就是传给Promise.reject()的第一个参数。
1
2
3
let p = Promise.reject(3); 
setTimeout(console.log, 0, p); // Promise <rejected>: 3
p.then(null, (e) => setTimeout(console.log, 0, e)); // 3
  • Promise.reject ()并没有照搬Promise.resolve()的幂等逻辑。如果传一个期约对象,则这个期约会成为它返回的拒绝期约的理由。
1
2
setTimeout(console.log, 0, Promise.reject(Promise.resolve())); 
// Promise <rejected>: Promise <resolved>

同步/异步执行的二元性

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

第一个try/catch 抛出并捕获了错误,第二个try/catch 抛出错误却没有捕获到。乍一看这可能有点违反直觉,因为代码中确实是同步创建了一个拒绝的期约实例,而这个实例也抛出了包含拒绝理由的错误。这里的同步代码之所以没有捕获期约抛出的错误,是因为它没有通过异步模式捕获错误。从这里就可以看出期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介