第11章 期约与异步函数

ECMAScript 6 新增了正式的 Promise(期约)引用类型,支持优雅地定义和组织异步逻辑。

1 异步编程

1.1 同步与异步

同步操作的例子可以是执行一次简单的数学计算:

let x = 3; 
x = x + 4;

操作系统会在栈内存上分配一个存储浮点数值的空间,然后针对这个值做一次数学计算,再把计算结果写回之前分配的内存中。所有这些指令都是在单个线程中按顺序执行的。

异步操作的例子可以是在定时回调中执行一次简单的数学计算:

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

这一次执行线程不知道 x 值何时会改变,因为这取决于回调何时从消息队列出列并执行。第二个指令
块(加操作及赋值操作)是由系统计时器触发的,这会生成一个入队执行的中断。为了让后续代码能够使用 x,异步执行的函数需要在更新 x 的值以后通知其他代码。

1.2 以往的异步编程模式

在早期的 JavaScript 中,只支持定义回调函数来表明异步操作完成。

function double(value) { 
 setTimeout(() => setTimeout(console.log, 0, value * 2), 1000); 
} 
double(3); 
// 6(大约 1000 毫秒之后)

setTimeout 可以定义一个在指定时间之后会被调度执行的回调函数。对这个例子而言,1000 毫秒之后,JavaScript 运行时会把回调函数推到自己的消息队列上去等待执行。

1.异步返回值

假设 setTimeout 操作会返回一个有用的值。要把这个值传给需要它的地方,一个策略是给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数)。

function double(value, callback) { 
 setTimeout(() => callback(value * 2), 1000); 
} 
double(3, (x) => console.log(`I was given: ${x}`)); 
// I was given: 6(大约 1000 毫秒之后)

2.失败处理

成功回调和失败回调:

function double(value, success, failure) { 
 setTimeout(() => { 
 try { 
 if (typeof value !== 'number') { 
 throw 'Must provide number as first argument'; 
 } 
 success(2 * value); 
 } catch (e) { 
 failure(e); 
 } 
 }, 1000); 
} 
const successCallback = (x) => console.log(`Success: ${x}`); 
const failureCallback = (e) => console.log(`Failure: ${e}`); 
double(3, successCallback, failureCallback); 
double('b', successCallback, failureCallback); 
// Success: 6(大约 1000 毫秒之后)
// Failure: Must provide number as first argument(大约 1000 毫秒之后)

这种模式已经不可取了,因为必须在初始化异步操作时定义回调。异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调才能接收到它。

3.嵌套异步回调

如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。在实际的代码中,这就要求嵌套回调,随着代码越来越复杂,形成“回调地狱”。

2 期约

期约是对尚不存在结果的一个替身。

2.1 Promises/A+规范

ECMAScript 6 增加了对 Promises/A+规范的完善支持,即 Promise 类型。成为了主导性的异步编程机制。

2.2 期约基础

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

let p = new Promise(() => {}); 
setTimeout(console.log, 0, p); // Promise <pending>

1.期约状态机

在把一个期约实例传给 console.log()时,控制台输出表明该实例处于待定(pending)状态。期约是一个有状态的对象,可能处于如下 3 种状态之一:

  • 待定(pending)
  • 兑现(fulfilled,有时候也称为“解决”,resolved)
  • 拒绝(rejected)

在待定状态下,期约可以落定为兑现或拒绝,无论落定为哪种状态都是不可逆的。期约的状态是私有的,不能直接通过 JavaScript 检测到。这主要是为了避免根据读取到的期约状态,以同步方式处理期约对象。另外,期约的状态也不能被外部 JavaScript 代码修改。这与不能读取该状态的原因是一样的:期约故意将异步行为封装起来,从而隔离外部的同步代码。

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

每个期约只要状态切换为兑现,就会有一个私有的内部值(value)。类似地,每个期约只要状态切换为拒绝,就会有一个私有的内部理由(reason)。

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

执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。其中,控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为 resolve()和 reject()。调用
resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。另外,调用 reject()也会抛出错误。

在初始化期约时,执行器函数已经改变了每个期约的状态。这里的关键在于,执行器函数是同步执行的。这是因为执行器函数是期约的初始化程序。通过下面的例子可以看出执行顺序:

new Promise(() => setTimeout(console.log, 0, 'executor')); 
setTimeout(console.log, 0, 'promise initialized'); 
// executor 
// promise initialized 

// 添加 setTimeout 可以推迟切换状态:
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000)); 
// 在 console.log 打印期约实例的时候,还不会执行超时回调(即 resolve())
setTimeout(console.log, 0, p); // Promise <pending>

4.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

5.Promise.reject()

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

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

这个拒绝的期约的理由就是传给 Promise.reject()的第一个参数。这个参数也会传给后续的拒绝处理程序:

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()的幂等逻辑。如果给它传一个期约对象,则这个期约会成为它返回的拒绝期约的理由:

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

6. 同步/异步执行的二元性

 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 块并不能捕获该错误。代码一旦开始以异步模式执行,则唯一与之交互的方式就是使用异步结构——更具体地说,就是期约的方法。

2.3 期约的实例方法

期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁。

1. 实现 Thenable 接口

ECMAScript 暴露的异步结构中,任何对象都有一个 then()方法。这个方法被认为实现了Thenable 接口。下面的例子展示了实现这一接口的最简单的类:

class MyThenable { 
 then() {} 
}

2.Promise.prototype.then()

Promise.prototype.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')); 
p2.then(() => onResolved('p2'), 
 () => onRejected('p2')); 
//(3 秒后)
// p1 resolved 
// p2 rejected

因为期约只能转换为最终状态一次,所以这两个操作一定是互斥的。
传给 then()的任何非函数类型的参数都会被静默忽略。如果想只提供 onRejected 参数,那就要在 onResolved 参数的位置上传入 undefined。

 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('gobbeltygook'); 
// 不传 onResolved 处理程序的规范写法
p2.then(null, () => onRejected('p2')); 
// p2 rejected(3 秒后)

Promise.prototype.then()方法返回一个新的期约实例:

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。

let p1 = Promise.resolve('foo'); 
// 若调用 then()时不传处理程序,则原样向后传
let p2 = p1.then();
setTimeout(console.log, 0, p2); // Promise <resolved>: foo
// 这些都一样
let p3 = p1.then(() => undefined); 
let p4 = p1.then(() => {}); 
let p5 = p1.then(() => Promise.resolve()); 
setTimeout(console.log, 0, p3); // Promise <resolved>: undefined 
setTimeout(console.log, 0, p4); // Promise <resolved>: undefined 
setTimeout(console.log, 0, p5); // Promise <resolved>: undefined

//如果有显式的返回值,则 Promise.resolve()会包装这个值:
// 这些都一样
let p6 = p1.then(() => 'bar'); 
let p7 = p1.then(() => Promise.resolve('bar')); 
setTimeout(console.log, 0, p6); // Promise <resolved>: bar 
setTimeout(console.log, 0, p7); // Promise <resolved>: bar

// Promise.resolve()保留返回的期约
let p8 = p1.then(() => new Promise(() => {})); 
let p9 = p1.then(() => Promise.reject()); 
// Uncaught (in promise): undefined 
setTimeout(console.log, 0, p8); // Promise <pending> 
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined

//抛出异常会返回拒绝的期约:
let p10 = p1.then(() => { throw 'baz'; }); 
// Uncaught (in promise) baz 
setTimeout(console.log, 0, p10); // Promise <rejected> baz

//注意,返回错误值不会触发上面的拒绝行为,而会把错误对象包装在一个解决的期约中:
let p11 = p1.then(() => Error('qux')); 
setTimeout(console.log, 0, p11); // Promise <resolved>: Error: qux

onRejected 处理程序也与之类似:onRejected 处理程序返回的值也会被 Promise.resolve()包装。onRejected 处理程序的任务就是捕获异步错误。
用 Promise.reject()替代之前例子中的 Promise.resolve()

let p1 = Promise.reject('foo'); 
// 调用 then()时不传处理程序则原样向后传
let p2 = p1.then(); 
// Uncaught (in promise) foo
setTimeout(console.log, 0, p2); // Promise <rejected>: foo

let p7 = p1.then(null, () => Promise.resolve('bar'));
setTimeout(console.log, 0, p7); // Promise <resolved>: bar

let p10 = p1.then(null, () => { throw 'baz'; }); 
// Uncaught (in promise) baz 
setTimeout(console.log, 0, p10); // Promise <rejected>: baz

3.Promise.prototype.catch()

Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数:onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype. then(null, onRejected)。

let p = Promise.reject(); 
let onRejected = function(e) { 
 setTimeout(console.log, 0, 'rejected'); 
}; 
// 这两种添加拒绝处理程序的方式是一样的:
p.then(null, onRejected); // rejected 
p.catch(onRejected); // rejected

//Promise.prototype.catch()返回一个新的期约实例:
let p1 = new Promise(() => {}); 
let p2 = p1.catch(); 
setTimeout(console.log, 0, p1); // Promise <pending> 
setTimeout(console.log, 0, p2); // Promise <pending> 
setTimeout(console.log, 0, p1 === p2); // false

4. Promise.prototype.finally()

这个方法用于给期约添加 onFinally 处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码。但 onFinally 处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码。

let p1 = Promise.resolve(); 
let p2 = Promise.reject(); 
let onFinally = function() { 
 setTimeout(console.log, 0, 'Finally!') 
} 
p1.finally(onFinally); // Finally 
p2.finally(onFinally); // Finally

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

这个新期约实例不同于 then()或 catch()方式返回的实例。因为 onFinally 被设计为一个状态无关的方法,所以在大多数情况下它将表现为父期约的传递。

let p1 = Promise.resolve('foo'); 
// 这里都会原样后传
let p2 = p1.finally(); 
let p3 = p1.finally(() => undefined); 
let p4 = p1.finally(() => {}); 
let p5 = p1.finally(() => Promise.resolve()); 
let p6 = p1.finally(() => 'bar'); 
let p7 = p1.finally(() => Promise.resolve('bar')); 
let p8 = p1.finally(() => Error('qux')); 
setTimeout(console.log, 0, p2); // Promise <resolved>: foo 
setTimeout(console.log, 0, p3); // Promise <resolved>: foo 
setTimeout(console.log, 0, p4); // Promise <resolved>: foo 
setTimeout(console.log, 0, p5); // Promise <resolved>: foo 
setTimeout(console.log, 0, p6); // Promise <resolved>: foo 
setTimeout(console.log, 0, p7); // Promise <resolved>: foo 
setTimeout(console.log, 0, p8); // Promise <resolved>: foo

// Promise.resolve()保留返回的期约
let p9 = p1.finally(() => new Promise(() => {})); 
let p10 = p1.finally(() => Promise.reject()); 
// Uncaught (in promise): undefined 
setTimeout(console.log, 0, p9); // Promise <pending> 
setTimeout(console.log, 0, p10); // Promise <rejected>: undefined 
let p11 = p1.finally(() => { throw 'baz'; }); 
// Uncaught (in promise) baz 
setTimeout(console.log, 0, p11); // Promise <rejected>: baz

5.非重入期约方法

当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处
理程序的代码之后的同步代码一定会在处理程序之前先执行。即使期约一开始就是与附加处理程序关联的状态,执行顺序也是这样的。这个特性由 JavaScript 运行时保证,被称为“非重入”(non-reentrancy)特性。下面的例子演示了这个特性:

// 创建解决的期约
let p = Promise.resolve(); 
// 添加解决处理程序
// 直觉上,这个处理程序会等期约一解决就执行
p.then(() => console.log('onResolved handler')); 
// 同步输出,证明 then()已经返回
console.log('then() returns'); 
// 实际的输出:
// then() returns 
// onResolved handler

在这个例子中,在一个解决期约上调用 then()会把 onResolved 处理程序推进消息队列。但这个处理程序在当前线程上的同步代码执行完成前不会执行。因此,跟在 then()后面的同步代码一定先于处理程序执行。

6.邻近处理程序的执行顺序

如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行。无论是 then()、catch()还是 finally()添加的处理程序都是如此。

let p1 = Promise.resolve(); 
let p2 = Promise.reject(); 
p1.then(() => setTimeout(console.log, 0, 1)); 
p1.then(() => setTimeout(console.log, 0, 2)); 
// 1 
// 2 
p2.then(null, () => setTimeout(console.log, 0, 3)); 
p2.then(null, () => setTimeout(console.log, 0, 4)); 
// 3 
// 4
p1.finally(() => setTimeout(console.log, 0, 7)); 
p1.finally(() => setTimeout(console.log, 0, 8)); 
// 7 
// 8

7.传递解决值和拒绝理由

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

8. 拒绝期约与拒绝错误处理

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

let p1 = new Promise((resolve, reject) => reject(Error('foo'))); 
let p2 = new Promise((resolve, reject) => { throw Error('foo'); }); 
let p3 = Promise.resolve().then(() => { throw Error('foo'); }); 
let p4 = Promise.reject(Error('foo')); 
setTimeout(console.log, 0, p1); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p2); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p3); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p4); // Promise <rejected>: Error: foo

2.4 期约连锁与期约合成

期约连锁与期约合成。前者就是一个期约接一个期约地拼接,后者则是将多个期约组合为一个期约。

1.期约连锁

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

这个实现最终执行了一连串同步任务。要真正执行异步任务,需要让每个执行器都返回一个期约实例。这样就可以让每个后续期约都等待之前的期约,也就是串行化异步任务。

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

把生成期约的代码提取到一个工厂函数中,就可以写成这样:

function delayedResolve(str) { 
 return new Promise((resolve, reject) => { 
 console.log(str); 
 setTimeout(resolve, 1000); 
 }); 
}
delayedResolve('p1 executor') 
 .then(() => delayedResolve('p2 executor')) 
 .then(() => delayedResolve('p3 executor')) 
 .then(() => delayedResolve('p4 executor')) 
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

2.期约图

期约连锁可以构建有向非循环图的结构。

3. Promise.all()和 Promise.race()

Promise 类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all()和 Promise.race()。而合成后期约的行为取决于内部期约的行为。

Promise.all()

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

let p1 = Promise.all([ 
 Promise.resolve(), 
 Promise.resolve() 
]); 
// 可迭代对象中的元素会通过 Promise.resolve()转换为期约
let p2 = Promise.all([3, 4]); 
// 空的可迭代对象等价于 Promise.resolve() 
let p3 = Promise.all([]); 
// 无效的语法
let p4 = Promise.all(); 
// TypeError: cannot read Symbol.iterator of undefined

合成的期约只会在每个包含的期约都解决之后才解决:

let p = Promise.all([ 
 Promise.resolve(), 
 new Promise((resolve, reject) => setTimeout(resolve, 1000)) 
]); 
setTimeout(console.log, 0, p); // Promise <pending> 
p.then(() => setTimeout(console.log, 0, 'all() resolved!')); 
// all() resolved!(大约 1 秒后)

如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的
期约也会拒绝。如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序。

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

let p1 = Promise.race([ 
 Promise.resolve(), 
 Promise.resolve() 
]); 
// 可迭代对象中的元素会通过 Promise.resolve()转换为期约
let p2 = Promise.race([3, 4]); 
// 空的可迭代对象等价于 new Promise(() => {}) 
let p3 = Promise.race([]); 
// 无效的语法
let p4 = Promise.race(); 
// TypeError: cannot read Symbol.iterator of undefined

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

4.串行期约合成

function addTwo(x) {return x + 2;} 
function addThree(x) {return x + 3;} 
function addFive(x) {return x + 5;} 
function addTen(x) { 
 return Promise.resolve(x) 
 .then(addTwo) 
 .then(addThree) 
 .then(addFive); 
} 
addTen(8).then(console.log); // 18

2.5 期约扩展

1.期约取消

提供一种临时性的封装,以实现取消期约的功能。这可以用到 “取消令牌”(cancel token)。生成的令牌实例提供了一个接口,利用这个接口可以取消期约;同时也提供了一个期约的实例,可以用来触发取消后的操作并求值取消状态。下面是 CancelToken 类的一个基本实例:

class CancelToken { 
 constructor(cancelFn) { 
	 this.promise = new Promise((resolve, reject) => { 
	 cancelFn(resolve); 
 }); 
 } 
}

2.期约进度通知

执行中的期约可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控期约的执行进度会很有用。ECMAScript 6 期约并不支持进度追踪,但是可以通过扩展来实现。一种实现方式是扩展 Promise 类,为它添加 notify()方法。

注意 ES6 不支持取消期约和进度通知,一个主要原因就是这样会导致期约连锁和期约合成
过度复杂化。比如在一个期约连锁中,如果某个被其他期约依赖的期约被取消了或者发出了
通知,那么接下来应该发生什么完全说不清楚。

3 异步函数

异步函数,也称为“async/await”(语法关键字),是 ES6 期约模式在 ECMAScript 函数中的应用。让以同步方式写的代码能够异步执行。

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
//上面这个期约在 1000 毫秒之后解决为数值 3。如果程序中的其他代码要在这个值可用时访问它,则需要
//写一个解决处理程序:
p.then((x) => console.log(x)); // 3

这其实是很不方便的,因为其他代码都必须塞到期约处理程序中。ES8 为此提供了 async/await 关键字。

3.1 异步函数

1.async

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

async function foo() {} 
let bar = async function() {}; 
let baz = async () => {}; 
class Qux { 
 async qux() {} 
}

使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的:

async function foo() { 
 console.log(1); 
} 
foo(); 
console.log(2); 
// 1 
// 2

异步函数如果使用 return 关键字返回了值(如果没有 return 则会返回 undefined),这个值会被 Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象。在函数外部调用这个函数可以得到它返回的期约:

async function foo() { 
 console.log(1); 
 return 3;  
 // return Promise.resolve(3); //  或者直接返回期约对象
} 
// 给返回的期约添加一个解决处理程序
foo().then(console.log);
console.log(2); 
// 1 
// 2 
// 3

2.await

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

// 1000 毫秒后异步打印"baz" 
async function baz() { 
 await new Promise((resolve, reject) => setTimeout(resolve, 1000)); 
 console.log('baz'); 
} 
baz(); 
// baz(1000 毫秒后)

对拒绝的期约使用 await 则会释放(unwrap)错误值(将拒绝期约返回):

async function foo() { 
 console.log(1); 
 await Promise.reject(3); 
 console.log(4); // 这行代码不会执行
} 
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log); 
console.log(2); 
// 1 
// 2 
// 3

3. await 的限制

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

3.2 停止和恢复执行

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.3 异步函数策略

1.实现 sleep()

一个简单的箭头函数就可以实现 sleep():

async function sleep(delay) { 
 return new Promise((resolve) => setTimeout(resolve, delay)); 
} 
async function foo() { 
 const t0 = Date.now(); 
 await sleep(1500); // 暂停约 1500 毫秒
 console.log(Date.now() - t0); 
} 
foo(); 
// 1502

2. 利用平行执行

3.串行执行期约

4.栈追踪与内存管理

期约与异步函数的功能有相当程度的重叠,但它们在内存中的表示则差别很大。看看下面的例子,它展示了拒绝期约的栈追踪信息:

function fooPromiseExecutor(resolve, reject) { 
 setTimeout(reject, 1000, 'bar'); 
} 
function foo() { 
 new Promise(fooPromiseExecutor); 
}
foo(); 
// Uncaught (in promise) bar 
// setTimeout 
// setTimeout (async) 
// fooPromiseExecutor 
// foo

如果在前面的例子中使用的是异步函数:

function fooPromiseExecutor(resolve, reject) { 
 setTimeout(reject, 1000, 'bar'); 
} 
async function foo() { 
 await new Promise(fooPromiseExecutor); 
} 
foo(); 
// Uncaught (in promise) bar 
// foo
// async function (async) 
// foo

这样一改,栈追踪信息就准确地反映了当前的调用栈。fooPromiseExecutor()已经返回,所以
它不在错误信息中。但 foo()此时被挂起了,并没有退出。JavaScript 运行时可以简单地在嵌套函数中
存储指向包含函数的指针,就跟对待同步函数调用栈一样。这个指针实际上存储在内存中,可用于在出错时生成栈追踪信息。这样就不会像之前的例子那样带来额外的消耗,因此在重视性能的应用中是可以优先考虑的。

4 小结

期约的主要功能是为异步代码提供了清晰的抽象。可以用期约表示异步执行的代码块,也可以用期约表示异步计算的值。在需要串行异步代码时,期约的价值最为突出。作为可塑性极强的一种结构,期约可以被序列化、连锁使用、复合、扩展和重组。

异步函数是将期约应用于 JavaScript 函数的结果。异步函数可以暂停执行,而不阻塞主线程。无论是编写基于期约的代码,还是组织串行或平行执行的异步代码,使用异步函数都非常得心应手。

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值