查看原文
其他

【第2068期】高级程序4:异步函数

李松峰 前端早读课 2021-03-09

前言

最近Promise被翻译成期约,大家好像有所讨论?今日早读文章由360@李松峰授权分享。

@李松峰,资深技术图书译者,翻译出版过40余部技术及交互设计专著,现任360奇舞团Web前端开发资深专家,360前端技术委员会委员、W3C AC代表。

正文从这开始~~

本文内容来自于李松峰老师翻译的《JavaScript高级程序设计》第 4 版第 11 章 - 期约与异步函数,本文只是第 11 章部分内容,文中提到的规约是指Promises。

异步函数,也称为“async/await”(语法关键字),是 ES6 期约模式在 ECMAScript 函数中的应用。async/await 是 ES8 规范新增的。这个特性从行为和语法上都增强了 JavaScript,让以同步方式写的代码能够异步执行。下面来看一个最简单的例子,这个期约在超时之后会解决为一个值:

  1. let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));

这个期约在 1000 毫秒之后解决为数值 3。如果程序中的其他代码要在这个值可用时访问它,则需要写一个解决处理程序:

  1. let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));


  2. p.then((x) => console.log(x)); // 3

这其实是很不方便的,因为其他代码都必须塞到期约处理程序中。不过可以把处理程序定义为一个函数

  1. function handler(x) { console.log(x); }


  2. let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));

  3. p.then(handler); // 3

这个改进其实也不大。这是因为任何需要访问这个期约所产生值的代码,都需要以处理程序的形式来接收这个值。也就是说,代码照样还是要放到处理程序里。ES8 为此提供了 async/await 关键字。

异步函数

ES8 的 async/await 旨在解决利用异步结构组织代码的问题。为此,ECMAScript 对函数进行了扩展,为其增加了两个新关键字:async 和 await。

async

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

  1. async function foo() {}

  2. let bar = async function() {};

  3. let baz = async () => {};


  4. class Qux {

  5. async qux() {}

  6. }

使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。而在参数或闭包方面,异步函数仍然具有普通 JavaScript 函数的正常行为。正如下面的例子所示,foo()函数仍然会在后面的指令之前被求值:

  1. async function foo() {

  2. console.log(1);

  3. }


  4. foo();

  5. console.log(2);

  6. // 1

  7. // 2

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

  1. async function foo() {

  2. console.log(1);

  3. return 3;

  4. }

  5. // 给返回的期约添加一个解决处理程序

  6. foo().then(console.log);

  7. console.log(2);

  8. // 1

  9. // 2

  10. // 3

当然,直接返回一个期约对象也是一样的:

  1. async function foo() {

  2. console.log(1);

  3. return Promise.resolve(3);

  4. }

  5. // 给返回的期约添加一个解决处理程序

  6. foo().then(console.log);


  7. console.log(2);


  8. // 1

  9. // 2

  10. // 3

异步函数的返回值期待(但实际上并不要求)一个实现 thenable 接口的对象,但常规的值也可以。如果返回的是实现 thenable 接口的对象,则这个对象可以由提供给 then()的处理程序“解包”。如果不是,则返回值就被当作已经解决的期约。下面的代码演示了这些情况:

  1. // 返回一个原始值

  2. async function foo() {

  3. return 'foo';

  4. }


  5. foo().then(console.log);

  6. // foo

  7. // 返回一个没有实现 thenable 接口的对象

  8. async function bar() {

  9. return ['bar'];

  10. }

  11. bar().then(console.log);

  12. // ['bar']


  13. // 返回一个实现了 thenable 接口的非期约对象

  14. async function baz() {

  15. const thenable = {

  16. then(callback) { callback('baz'); }

  17. };

  18. return thenable;

  19. }


  20. baz().then(console.log);

  21. // baz

  22. // 返回一个期约

  23. async function qux() {

  24. return Promise.resolve('qux');

  25. }

  26. qux().then(console.log);

  27. // qux

与在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约:

  1. async function foo() {

  2. console.log(1);

  3. throw 3;

  4. }

  5. // 给返回的期约添加一个拒绝处理程序

  6. foo().catch(console.log);

  7. console.log(2);

  8. // 1

  9. // 2

  10. // 3

不过,拒绝期约的错误不会被异步函数捕获:

  1. async function foo() {

  2. console.log(1);

  3. Promise.reject(3);

  4. }


  5. // Attach a rejected handler to the returned promise

  6. foo().catch(console.log);

  7. console.log(2);

  8. // 1

  9. // 2

  10. // Uncaught (in promise): 3

await

因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用 await 关键字可以暂停异步函数代码的执行,等待期约解决。来看下面这个本章开始就出现过的例子:

  1. let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));


  2. p.then((x) => console.log(x)); // 3

使用 async/await 可以写成这样:

  1. async function foo() {

  2. let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));

  3. console.log(await p);

  4. }


  5. foo();

  6. // 3

注意,await 关键字会暂停执行异步函数后面的代码,让出 JavaScript 运行时的执行线程。这个行 为与生成器函数中的 yield 关键字是一样的。await 关键字同样是尝试“解包”对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行。

await 关键字的用法与 JavaScript 的一元操作一样。它可以单独使用,也可以在表达式中使用,如下面的例子所示:

  1. // 异步打印"foo"

  2. async function foo() {

  3. console.log(await Promise.resolve('foo'));

  4. }

  5. foo();

  6. // foo


  7. // 异步打印"bar"

  8. async function bar() {

  9. return await Promise.resolve('bar');

  10. }


  11. bar().then(console.log);

  12. // bar

  13. // 1000 毫秒后异步打印"baz"

  14. async function baz() {

  15. await new Promise((resolve, reject) => setTimeout(resolve, 1000));

  16. console.log('baz');

  17. }

  18. baz();

  19. // baz(1000 毫秒后)

await 关键字期待(但实际上并不要求)一个实现 thenable 接口的对象,但常规的值也可以。如 果是实现 thenable 接口的对象,则这个对象可以由 await 来“解包”。如果不是,则这个值就被当作已经解决的期约。下面的代码演示了这些情况:

  1. // 等待一个原始值

  2. async function foo() {

  3. console.log(await 'foo');

  4. }


  5. foo();

  6. // foo


  7. // 等待一个没有实现 thenable 接口的对象

  8. async function bar() {

  9. console.log(await ['bar']);

  10. }

  11. bar();

  12. // ['bar']


  13. // 等待一个实现了 thenable 接口的非期约对象

  14. async function baz() {

  15. const thenable = {

  16. then(callback) { callback('baz');}

  17. };

  18. console.log(await thenable);

  19. }

  20. baz();

  21. // baz


  22. // 等待一个期约

  23. async function qux() {

  24. console.log(await Promise.resolve('qux'));

  25. }

  26. qux();

  27. // qux

等待会抛出错误的同步操作,会返回拒绝的期约:

  1. async function foo() {

  2. console.log(1);

  3. await (() => { throw 3; })();

  4. }

  5. // 给返回的期约添加一个拒绝处理程序

  6. foo().catch(console.log);

  7. console.log(2);

  8. // 1

  9. // 2

  10. // 3

如前面的例子所示,单独的 Promise.reject()不会被异步函数捕获,而会抛出未捕获错误。不过,对拒绝的期约使用 await 则会释放(unwrap)错误值(将拒绝期约返回):

  1. async function foo() {

  2. console.log(1);

  3. await Promise.reject(3);

  4. console.log(4); // 这行代码不会执行

  5. }


  6. // 给返回的期约添加一个拒绝处理程序

  7. foo().catch(console.log);

  8. console.log(2);

  9. // 1

  10. // 2

  11. // 3

await 的限制

await 关键字必须在异步函数中使用,不能在顶级上下文如 <script>标签或模块中使用。不过,定义并立即调用异步函数是没问题的。下面两段代码实际是相同的:

  1. async function foo() {

  2. console.log(await Promise.resolve(3));

  3. }

  4. foo();

  5. // 3


  6. // 立即调用的异步[函数](http://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651223317&idx=1&sn=87da37c1d46c2b75da9fbbb8306a76e4&chksm=bd49ac918a3e2587622147dbfa7aaaa472a964023d904ace05a1ba1ea557a9b52b5d09ebf95b&scene=4&subscene=126#wechat_redirect)表达式

  7. (async function() {

  8. console.log(await Promise.resolve(3));

  9. })();

  10. // 3

此外,异步函数的特质不会扩展到嵌套函数。因此,await 关键字也只能直接出现在异步函数的定 义中。在同步函数内部使用 await 会抛出 SyntaxError。

下面展示了一些会出错的例子:

  1. // 不允许:await 出现在了箭头[函数](http://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651223317&idx=1&sn=87da37c1d46c2b75da9fbbb8306a76e4&chksm=bd49ac918a3e2587622147dbfa7aaaa472a964023d904ace05a1ba1ea557a9b52b5d09ebf95b&scene=4&subscene=126#wechat_redirect)中

  2. function foo() {

  3. const syncFn = () => {

  4. return await Promise.resolve('foo');

  5. };

  6. console.log(syncFn());

  7. }

  8. // 不允许:await 出现在了同步[函数](http://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651223317&idx=1&sn=87da37c1d46c2b75da9fbbb8306a76e4&chksm=bd49ac918a3e2587622147dbfa7aaaa472a964023d904ace05a1ba1ea557a9b52b5d09ebf95b&scene=4&subscene=126#wechat_redirect)声明中

  9. function bar() {

  10. function syncFn() {

  11. return await Promise.resolve('bar');

  12. }

  13. console.log(syncFn());

  14. }


  15. // 不允许:await 出现在了同步[函数](http://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651223317&idx=1&sn=87da37c1d46c2b75da9fbbb8306a76e4&chksm=bd49ac918a3e2587622147dbfa7aaaa472a964023d904ace05a1ba1ea557a9b52b5d09ebf95b&scene=4&subscene=126#wechat_redirect)表达式中

  16. function baz() {

  17. const syncFn = function() {

  18. return await Promise.resolve('baz');

  19. };

  20. console.log(syncFn());

  21. }


  22. // 不允许:IIFE 使用同步[函数](http://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651223317&idx=1&sn=87da37c1d46c2b75da9fbbb8306a76e4&chksm=bd49ac918a3e2587622147dbfa7aaaa472a964023d904ace05a1ba1ea557a9b52b5d09ebf95b&scene=4&subscene=126#wechat_redirect)表达式或箭头[函数](http://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651223317&idx=1&sn=87da37c1d46c2b75da9fbbb8306a76e4&chksm=bd49ac918a3e2587622147dbfa7aaaa472a964023d904ace05a1ba1ea557a9b52b5d09ebf95b&scene=4&subscene=126#wechat_redirect)

  23. function qux() {

  24. (function () { console.log(await Promise.resolve('qux')); })();

  25. (() => console.log(await Promise.resolve('qux')))();

  26. }

停止和恢复执行

使用 await 关键字之后的区别其实比看上去的还要微妙一些。比如,下面的例子中按顺序调用了 3个函数,但它们的输出结果顺序是相反的:

  1. async function foo() {

  2. console.log(await Promise.resolve('foo'));

  3. }


  4. async function bar() {

  5. console.log(await 'bar');

  6. }

  7. async function baz() {

  8. console.log('baz');

  9. }


  10. foo();

  11. bar();

  12. baz();


  13. // baz

  14. // bar

  15. // foo

async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。毕竟,异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别:

  1. async function foo() {

  2. console.log(2);

  3. }


  4. console.log(1);

  5. foo();

  6. console.log(3);

  7. // 1

  8. // 2

  9. // 3

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

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

  1. async function foo() {

  2. console.log(2);

  3. await null;

  4. console.log(4);

  5. }


  6. console.log(1);

  7. foo();

  8. console.log(3);


  9. // 1

  10. // 2

  11. // 3

  12. // 4

控制台中输出结果的顺序很好地解释了运行时的工作过程:

  • 打印 1;

  • 调用异步函数 foo();

  • (在 foo()中)打印 2;

  • (在 foo()中)await 关键字暂停执行,为立即可用的值 null 向消息队列中添加一个任务;(5) foo()退出;

  • 打印 3;

  • 同步线程的代码执行完毕;

  • JavaScript 运行时从消息队列中取出任务,恢复异步函数执行;

  • (在 foo()中)恢复执行,await 取得 null 值(这里并没有使用);

  • (在 foo()中)打印 4;

  • foo()返回。

如果 await 后面是一个期约,则问题会稍微复杂一些。此时,为了执行异步函数,实际上会有两个任务被添加到消息队列并被异步求值。下面的例子虽然看起来很反直觉,但它演示了真正的执行顺序:

  1. async function foo() {

  2. console.log(2);

  3. console.log(await Promise.resolve(8));

  4. console.log(9);

  5. }


  6. async function bar() {

  7. console.log(4);

  8. console.log(await 6);

  9. console.log(7);

  10. }


  11. console.log(1);

  12. foo();

  13. console.log(3);

  14. bar();

  15. console.log(5);


  16. // 1

  17. // 2

  18. // 3

  19. // 4

  20. // 5

  21. // 6

  22. // 7

  23. // 8

  24. // 9

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

  • 打印 1;

  • 调用异步函数 foo();

  • (在 foo()中)打印 2;

  • (在 foo()中)await 关键字暂停执行,向消息队列中添加一个期约在落定之后执行的任务;

  • 期约立即落定,把给 await 提供值的任务添加到消息队列;

  • foo()退出;

  • 打印 3;

  • 调用异步函数 bar();

  • (在 bar()中)打印 4;

  • (在 bar()中)await 关键字暂停执行,为立即可用的值 6 向消息队列中添加一个任务;

  • bar()退出;

  • 打印 5;

  • 顶级线程执行完毕;

  • JavaScript 运行时从消息队列中取出解决 await 期约的处理程序,并将解决的值 8 提供给它;

  • JavaScript 运行时向消息队列中添加一个恢复执行 foo()函数的任务;

  • JavaScript 运行时从消息队列中取出恢复执行 bar()的任务及值 6; 11(17)(在 bar()中)恢复执行,await 取得值 6;

  • (在 bar()中)打印 6;

  • (在 bar()中)打印 7; 12(20) bar()返回;

  • 异步任务完成,JavaScript 从消息队列中取出恢复执行 foo()的任务及值 8;

  • (在 foo()中)打印 8;

  • (在 foo()中)打印 9;

  • foo()返回。

异步函数策略

因为简单实用,所以异步函数很快成为 JavaScript 项目使用最广泛的特性之一。不过,在使用异步函数时,还是有些问题要注意。

实现 sleep()

很多人在刚开始学习 JavaScript 时,想找到一个类似 Java 中 Thread.sleep()之类的函数,好在程 序中加入非阻塞的暂停。以前,这个需求基本上都通过 setTimeout()利用 JavaScript 运行时的行为来实现的。

有了异步函数之后,就不一样了。一个简单的箭头函数就可以实现 sleep():

  1. async function sleep(delay) {

  2. return new Promise((resolve) => setTimeout(resolve, delay));

  3. }


  4. async function foo() {

  5. const t0 = Date.now();

  6. await sleep(1500); // 暂停约 1500 毫秒

  7. console.log(Date.now() - t0);

  8. }

  9. foo();

  10. // 1502

利用平行执行

如果使用 await 时不留心,则很可能错过平行加速的机会。来看下面的例子,其中顺序等待了 5个随机的超时:

  1. async function randomDelay(id) {

  2. // 延迟 0~1000 毫秒

  3. const delay = Math.random() * 1000;

  4. return new Promise((resolve) => setTimeout(() => {

  5. console.log(`${id} finished`);

  6. resolve();

  7. }, delay));

  8. }


  9. async function foo() {

  10. const t0 = Date.now();

  11. await randomDelay(0);

  12. await randomDelay(1);

  13. await randomDelay(2);

  14. await randomDelay(3);

  15. await randomDelay(4);

  16. console.log(`${Date.now() - t0}ms elapsed`);

  17. }

  18. foo();


  19. // 0 finished

  20. // 1 finished

  21. // 2 finished

  22. // 3 finished

  23. // 4 finished

  24. // 2219ms elapsed

用一个 for 循环重写,就是:

  1. async function randomDelay(id) {

  2. // 延迟 0~1000 毫秒

  3. const delay = Math.random() * 1000;

  4. return new Promise((resolve) => setTimeout(() => {

  5. console.log(`${id} finished`);

  6. resolve();

  7. }, delay));

  8. }


  9. async function foo() {

  10. const t0 = Date.now();

  11. for (let i = 0; i < 5; ++i) {

  12. await randomDelay(i);

  13. }

  14. console.log(`${Date.now() - t0}ms elapsed`);

  15. }

  16. foo();


  17. // 0 finished

  18. // 1 finished

  19. // 2 finished

  20. // 3 finished

  21. // 4 finished

  22. // 2219ms elapsed

就算这些期约之间没有依赖,异步函数也会依次暂停,等待每个超时完成。这样可以保证执行顺序, 但总执行时间会变长。

如果顺序不是必需保证的,那么可以先一次性初始化所有期约,然后再分别等待它们的结果。比如:

  1. async function randomDelay(id) {

  2. // 延迟 0~1000 毫秒

  3. const delay = Math.random() * 1000;

  4. return new Promise((resolve) => setTimeout(() => {

  5. setTimeout(console.log, 0, `${id} finished`);

  6. resolve();

  7. }, delay));

  8. }


  9. async function foo() {

  10. const t0 = Date.now();


  11. const p0 = randomDelay(0);

  12. const p1 = randomDelay(1);

  13. const p2 = randomDelay(2);

  14. const p3 = randomDelay(3);

  15. const p4 = randomDelay(4);


  16. await p0;

  17. await p1;

  18. await p2;

  19. await p3;

  20. await p4;


  21. setTimeout(console.log, 0,`${Date.now() - t0}ms elapsed`);

  22. }

  23. foo();


  24. // 1 finished

  25. // 4 finished

  26. // 3 finished

  27. // 0 finished

  28. // 2 finished

  29. // 2219ms elapsed

用数组和 for 循环再包装一下就是:

  1. async function randomDelay(id) {

  2. // 延迟 0~1000 毫秒

  3. const delay = Math.random() * 1000;

  4. return new Promise((resolve) => setTimeout(() => {

  5. console.log(`${id} finished`);

  6. resolve();

  7. }, delay));

  8. }


  9. async function foo() {

  10. const t0 = Date.now();

  11. const promises = Array(5).fill(null).map((_, i) => randomDelay(i));

  12. for (const p of promises) {

  13. await p;

  14. }

  15. console.log(`${Date.now() - t0}ms elapsed`);

  16. }


  17. foo();

  18. // 4 finished

  19. // 2 finished

  20. // 1 finished

  21. // 0 finished

  22. // 3 finished

  23. // 877ms elapsed

注意,虽然期约没有按照顺序执行,但 await 按顺序收到了每个期约的值:

  1. async function randomDelay(id) {

  2. // 延迟 0~1000 毫秒

  3. const delay = Math.random() * 1000;

  4. return new Promise((resolve) => setTimeout(() => {

  5. console.log(`${id} finished`);

  6. resolve(id);

  7. }, delay));

  8. }


  9. async function foo() {

  10. const t0 = Date.now();

  11. const promises = Array(5).fill(null).map((_, i) => randomDelay(i));

  12. for (const p of promises) {

  13. console.log(`awaited ${await p}`);

  14. }

  15. console.log(`${Date.now() - t0}ms elapsed`);

  16. }


  17. foo();

  18. // 1 finished

  19. // 2 finished

  20. // 4 finished

  21. // 3 finished

  22. // 0 finished

  23. // awaited 0

  24. // awaited 1

  25. // awaited 2

  26. // awaited 3

  27. // awaited 4

  28. // 645ms elapsed

串行执行期约

在 11.2 节,我们讨论过如何串行执行期约并把值传给后续的期约。使用 async/await,期约连锁会变得很简单:

  1. function addTwo(x) {return x + 2;}

  2. function addThree(x) {return x + 3;}

  3. function addFive(x) {return x + 5;}


  4. async function addTen(x) {

  5. for (const fn of [addTwo, addThree, addFive]) {

  6. x = await fn(x);

  7. }

  8. return x;

  9. }

  10. addTen(9).then(console.log); // 19

这里,await 直接传递了每个函数的返回值,结果通过迭代产生。当然,这个例子并没有使用期约, 如果要使用期约,则可以把所有函数都改成异步函数。这样它们就都返回期约了: 8

  1. async function addTwo(x) {return x + 2;}

  2. async function addThree(x) {return x + 3;}

  3. async function addFive(x) {return x + 5;}


  4. async function addTen(x) {

  5. for (const fn of [addTwo, addThree, addFive]) {

  6. x = await fn(x);

  7. }

  8. return x;

  9. }


  10. addTen(9).then(console.log); // 19

栈追踪与内存管理

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

  1. function fooPromiseExecutor(resolve, reject) {

  2. setTimeout(reject, 1000, 'bar');

  3. }

  4. function foo() {

  5. new Promise(fooPromiseExecutor);

  6. }

  7. foo();

  8. // Uncaught (in promise) bar

  9. // setTimeout

  10. // setTimeout (async)

  11. // fooPromiseExecutor

  12. // foo

根据对期约的不同理解程度,以上栈追踪信息可能会让某些读者不解。栈追踪信息应该相当直接地 表现 JavaScript 引擎当前栈内存中函数调用之间的嵌套关系。在超时处理程序执行时和拒绝期约时,我 们看到的错误信息包含嵌套函数的标识符,那是被调用以创建最初期约实例的函数。可是,我们知道这 些函数已经返回了,因此栈追踪信息中不应该看到它们。

答案很简单,这是因为 JavaScript 引擎会在创建期约时尽可能保留完整的调用栈。在抛出错误时, 调用栈可以由运行时的错误处理逻辑获取,因而就会出现在栈追踪信息中。当然,这意味着栈追踪信息 会占用内存,从而带来一些计算和存储成本。

如果在前面的例子中使用的是异步函数,那又会怎样呢?比如:

  1. function fooPromiseExecutor(resolve, reject) {

  2. setTimeout(reject, 1000, 'bar');

  3. }


  4. async function foo() {

  5. await new Promise(fooPromiseExecutor);

  6. }

  7. foo();


  8. // Uncaught (in promise) bar

  9. // foo

  10. // async function (async)

  11. // foo

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

小结

长期以来,掌握单线程 JavaScript 运行时的异步行为一直都是个艰巨的任务。随着 ES6 新增了期约和 ES8 新增了异步函数,ECMAScript 的异步编程特性有了长足的进步。通过期约和 async/await,不仅 可以实现之前难以实现或不可能实现的任务,而且也能写出更清晰、简洁,并且容易理解、调试的代码。

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

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

关于本文 作者:@李松峰 原文:https://mp.weixin.qq.com/s/kIrGqSpd1ZnIRhB0o49_ag

@李松峰老师曾分享过


【第1934期】前端最佳实践之可维护性


【第1566期】CSS捡屎记


最后,对着有情怀的javascript高级程序4,你有兴趣了解么?

添加早读君微信: zhgb_f2er,领取购书优惠券,数量有限

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存