相比传统回调函数,使用 Promise 的方式处理异步调用最大的优势是通过链式调用解决回调嵌套过深的问题。

使用 Promise 去处理处理异步任务执行表现形式就是一个 then 处理一个异步调用,整体形成一个任务链条,最终实现异步任务的串联执行:

  1. ajax('/api/url1')
  2. .then(value => {
  3. return ajax('/api/url2');
  4. })
  5. .then(value => {
  6. return ajax('/api/url3');
  7. })
  8. .then(value => {
  9. return ajax('/api/url4');
  10. })
  11. .catch(err => {
  12. console.error(err);
  13. });

但是这样写还是存在大量的回调函数,虽然他们之间没有相互嵌套,但是没有办法达到传统同步代码那种可读性。

如果像同步代码般,应该是像这种形式的样子:

  1. try {
  2. const value1 = ajax('/api/url1');
  3. console.log(value1);
  4. const value2 = ajax('/api/url2');
  5. console.log(value2);
  6. const value3 = ajax('/api/url3');
  7. console.log(value3);
  8. const value4 = ajax('/api/url4');
  9. console.log(value4);
  10. } catch (err) {
  11. console.error(err);
  12. }

ES6 提供的 Generator 生成器函数

  1. 普通的函数 function 后多了 *
  2. 当调用生成器函数时,并不会立即执行这个函数,而是得到一个生成器对象(又称迭代器)
  3. 直到调用生成器对象的 next() 方法,这个函数的函数体才会开始执行
  • 其次可以在生成器函数中随时使用 yield 关键词来向外返回一个值,可以通过生成器对象 next() 方法拿到这个值
  • yield 不会像 return 立即结束这个函数运行,只是暂停这个生成器的执行,直到外界下一次再调用生成器对象 next() 方法时,他继续从 yield 这个位置往下执行
  1. 生成器对象还有一个 done 属性来表示这个生成器是否全部运行完了
  2. 在调用生成器对象 next 方法,传入一个参数,会把这个参数作为 yield 语句的返回值。
  3. 在外部调用生成器对象 throw 方法,可对生成器内部抛出一个异常并可以使用 try … catch 来捕获 ```javascript function * foo() { console.log(‘start’);

    try { const res = yield ‘foo’; console.log(res); // bar } catch(e) { console.error(e); // Generator error } }

const generator = foo();

const result = generator.next(); console.log(result)

// generator.next(‘bar’);

generator.throw(new Error(‘Generator error’));

  1. <a name="PT1Zg"></a>
  2. # 利用生成器函数实现更优的异步编程体验
  3. 简单的 CO 函数
  4. ```javascript
  5. function * main() {
  6. try {
  7. const value1 = yield ajax('/api/url1');
  8. console.log(value1);
  9. const value2 = yield ajax('/api/url2');
  10. console.log(value2);
  11. const value3 = yield ajax('/api/url3');
  12. console.log(value3);
  13. const value4 = yield ajax('/api/url4');
  14. console.log(value4);
  15. } catch (err) {
  16. console.error(err);
  17. }
  18. }
  19. /*
  20. const g = main();
  21. const result = g.next();
  22. result.value.then(data => {
  23. const result2 = g.next(data);
  24. if(result2.done) return;
  25. result2.value.then(data => {
  26. const result3 = g.next(data);
  27. if(result3.done) return;
  28. result3.value.then(data => {
  29. const result4 = g.next(data);
  30. if(result4.done) return;
  31. // ...
  32. });
  33. }
  34. })
  35. */
  36. function co(generator){
  37. const g = generator();
  38. // 使用递归实现一个更通用的生成器函数的执行器
  39. // result 是生成器函数返回的生成器对象
  40. function handleResult (result) {
  41. if(result.done) return; // 生成器函数结束
  42. result.value.then(data => { // result.value 必须是 Promise
  43. handleResult(g.next(data));
  44. }, error => {
  45. g.throw(error);
  46. })
  47. }
  48. handleResult(g.next()); // 在外部调用 handleResult 并传入第一次 next 的结果
  49. // 后面只要这个生成器不结束,就把生成器函数中异步调用全部一直地执行下去
  50. }
  51. co(main);

https://github.com/tj/co

async / await 语法糖

有了 Generator 过后,JavaScript 中的异步编程就基本与同步代码有类似的体验了。但是使用 Generator 这种异步方案,还需要自己编写一个 CO 执行器函数,比较麻烦。
在 ES 2017 中新增一个 async 函数,同步提供了这种扁平化异步编程体验。而且是在语言层面的标准的异步编程语法,所以使用起来相当方便。其本质是生成器函数的 CO 执行函数,是一种使其使用更方便的语法糖。

async function main() {
  try    {
    const value1 = await ajax('/api/url1');
    console.log(value1);
    const value2 = await ajax('/api/url2');
    console.log(value2);
    const value3 = await ajax('/api/url3');
    console.log(value3);
    const value4 = await ajax('/api/url4');
    console.log(value4);
  } catch (err) {
      console.error(err);
  }
}

const promise = main();
promise.then(() => {
    console.log('all completed');
});
  1. async 最大的好处是不需要配合一个类似 CO 执行器函数, 因为他是语言层面上标准异步编程语法
  2. async 函数会返回一个 Promise 对象,更加利于对整体代码的控制

    目前 await 关键词只能出现在 async 函数当中