js ES6 异步处理之 Promise 函数、Generator 函数 与 async 函数

目录

前言

一、Promise 对象

1、Promise 概述

2、Promise 构造函数

(1)、创建一个 Promise 实例

(2)、Promise 函数接受的参数——resolve 函数和 reject 函数

(3)、Promise 函数的执行

(4)、promise 函数的链式调用

3、Promise 构造函数的实例方法

(1)、Promise.prototype.then()

(2)、Promise.prototype.catch()

(3)、Promise.prototype.finally()

4、Promise 构造函数自身的方法

(1)、Promise.all()

(2)、Promise.race()

(3)、Promise.allSettled()

(4)、Promise.any()

(5)、Promise.resolve()

(6)、Promise.reject()

(7)、Promise.try()

5、Promise 构造函数的应用

(1)、Promise + fetch 获取数据

 (2)、异步加载图片

(3)、Promise 与 Generator 函数结合使用

6、Promise 构造函数的问题

7、手写 Promise 构造函数

二、Generator 函数

1、Generator 函数是 ES6 对协程的实现

2、Generator 函数的语法

(1)、Generator 函数组成

(2)、Generator 函数的执行机制

(3)、作为对象属性的 Generator 函数

(4)、Generator 函数的作用域

(5)、Generator 函数的注意事项

3、yield* 表达式

(1)、yield* 表达式与 yield 表达式

(2)、yield*表达式的特点

4、Generator 函数返回的遍历器对象的方法

(1)、Generator.prototype.next()

(2)、Generator.prototype.throw()

(3)、Generator.prototype.return()

5、Generator 函数的应用

(1)、异步操作的同步化表达

(2)、控制流管理

(3)、部署 Iterator 接口

6、Generator 函数的自动执行

(1)、Thunk 函数

(2)、co 模块

三、async 函数

1、async 函数的语法

(1)、创建一个 async 函数

(2)、async 函数的调用

(3)、async 函数的 await 命令

(4)、async 函数的执行

(5)、async 函数的错误处理

(6)、注意事项

2、async 函数的实现原理

3、async 函数与 Promise 对象、Generator 函数的比较

4、顶层 await

四、使用实记

1、fetch 与 Promise 的使用案例

2、async-await 与 Promise 的使用案例

五、踩坑记


前言

async/await 和 Generator 都是 Promise 的语法糖,越来越甜了。

一、Promise 对象

1、Promise 概述

Promise 是异步编程的一种解决方案。

Promise 任务是微任务(事件任务相关请戳这里)。

Promise 本身是同步的,他的 then() 方法和 catch() 方法是异步的。

Promise 对象的状态:

  • Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。
  • Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。
const p1 = new Promise(function(resolve,reject){
    resolve('success1');
    resolve('success2');
}); 
const p2 = new Promise(function(resolve,reject){  
    resolve('success3'); 
    reject('reject');
});
p1.then(function(value){  
    console.log(value); // success1
});
p2.then(function(value){ 
    console.log(value); // success3
});

Promise 对象的优点:

  • 有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
  • Promise对象提供统一的接口,使得控制异步操作更加容易。

Promise 对象的缺点:

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署Promise更好的选择。

2、Promise 构造函数

(1)、创建一个 Promise 实例

Promise 是一个构造函数,用来生成 Promise 实例。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

(2)、Promise 函数接受的参数——resolve 函数和 reject 函数

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject 。

resolve 和 reject 是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数:

  • 用来将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
  • 可以接受一个参数:(可选的)一个值或另一个Promise对象。

reject函数:

  • 用来将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
  • 可以接受一个参数:Error对象的实例,表示抛出的错误。

关于promise中reject和catch的问题

①、resolve 函数接收一个值,比如:接收一个字符串。

const p = new Promise(function (resolve, reject) {
    resolve(1);
});
p.then(result => console.log(2));
// 2

上述代码中,可以给resolve函数预设一个参数值,也可以在then() 方法中指定一个参数值,后者会覆盖前者。

②、resolve 函数接收另一个 Promise 对象作为参数。

// 案例一
const p1 = new Promise(function (resolve, reject) {
    setTimeout(() => resolve("successful"), 3000)
});

const p2 = new Promise(function (resolve, reject) {
    resolve(p1);
})
p2.then(result => console.log(result));
// successful

// 案例二
const p1 = new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
    resolve(p1);
})

p2.then(result => console.log(result), error => console.log(error))
// Error: fail

上面代码中,p1和p2都是 Promise 的实例,但是p2的resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。

③、调用 resolve 或 reject 并不会终结 Promise 的参数函数的执行。

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

上面代码中,调用 resolve(1) 以后,后面的 console.log(2) 还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。

一般来说,调用 resolve 或 reject 以后,Promise 的使命就完成了,后继操作应该放到 then() 方法里面,而不应该直接写在 resolve 或 reject 的后面。所以,最好在它们前面加上 return 语句,这样就不会有意外:

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})

(3)、Promise 函数的执行

Promise 新建后立即执行,其回调函数会异步执行,无法中途取消。

new Promise((resolve, reject) => {
    console.log(1);
    resolve(2);
    console.log(3);
}).then((res) => console.log(4));

console.log(5);
// 1
// 3
// 5
// 4(2 被 4 覆盖了)

上面代码中,因为 Promise 创建后立即执行,所以首先输出的是1。因为 Promise 的回调函数会异步执行,将在当前脚本所有同步任务执行完才会执行,并且,由于调用 resolve 或 reject 并不会终结,所以接着输出 3。然后是 5 ,至此,所有同步脚本执行完毕。最后再去执行异步脚本,输出 4,没有输出 2 是因为传入的 4 将 2 覆盖了。

(4)、promise 函数的链式调用

如果依次读取两个以上的文件,就会出现多重嵌套,不便于实现与管理代码,代码的可读性差。因为多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为"回调函数地狱"(callback hell)。

Promise 对象就是为了解决这个问题而提出的。允许将回调函数的嵌套,改成链式调用。采用 Promise,连续读取多个文件。

var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});

上面代码中,我使用了fs-readfile-promise模块,它的作用就是返回一个 Promise 版本的readFile函数。Promise 提供then方法加载回调函数,catch方法捕捉执行过程中抛出的错误。

3、Promise 构造函数的实例方法

  • Promise.prototype.then():当 promise 执行后返回 fullfilled 状态时执行该函数,该函数里接收并执行一个 resolve 函数。用于执行 “成功” 后的处理。
  • Promise.prototype.catch():当 promise 执行后返回 rejected 状态时执行该函数,该函数里接收并执行一个 reject 函数。用于执行 “出错” 后的处理。
  • Promise.prototype.finally():无论 promise 执行的结果是成功还是失败,都会执行该函数里的回调函数。

(1)、Promise.prototype.then()

then() 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调;第二个参数是(可选的) Promise 执行失败时的回调,用来抛出错误。两个函数只会有一个被调用。then() 方法总是返回一个新的 Promise 实例。

promise.then(function(result) {
    console.log(result);
}, function(error) {
    console.log(error);
});

// 用箭头函数简写为
promise.then(result => console.log(result), error => console.log(error));

上述代码中,一个 promise 对象调用 then() 方法,如果该 promise 对象的状态变为resolved,就调用第一个回调函数,如果该 promise 对象的状态变为rejected,就调用第二个回调函数。最终返回一个新的 Promise 实例。

由于 then() 方法总是返回一个新的 Promise 实例,所以,then() 方法可以采用链式写法,用来指定一组按照次序调用的回调函数。

const p = new Promise(function(resolve, reject){
    resolve(1);

})
    .then(value => { 
        console.log(value);                                  // 第一个then // 1
        return value * 2;

    })
        .then(value => { 
            console.log(value);                              // 第二个then // 2

        })
            .then(value => {
                console.log(value);                          // 第三个then // undefined
                // 嵌套 Promise
                return Promise.resolve('resolve'); 

            })
                .then(value => {
                    console.log(value);                      // 第四个then // resolve
                    throw new Error('reject');

                })
                    .then(value => {
                        console.log( value);
                    }, err => {
                        console.log(err);                    // 最后一个then //Error: reject
                    });

由上面的代码可知,then 方法会返回一个 resolved 或 rejected 状态的 Promise 对象用于链式调用,且 Promise 对象的值就是这个返回值。

使用 then() 方法的注意事项:

  • 简便的 Promise 链式编程最好保持扁平化,尽量不要嵌套 Promise,以提高代码的可读性。
  • 要总是返回或终止 Promise 链。大多数浏览器中不能终止的 Promise 链里的 reject,建议后面都跟上 .catch(error => console.log(error))。

(2)、Promise.prototype.catch()

catch() 方法是then(null, rejection)或then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

该方法可以接受一个参数:一个 Error 对象的实例。

该方法总是返回一个新的 Promise 实例。

// 写法一
const promise = new Promise(function(resolve, reject) {
    throw new Error('test');
}).then(val => console.log('resolved:', val))
    .catch(error => console.log(error));
    // Error: test

// 写法二
const promise = new Promise(function(resolve, reject) {
    return reject(new Error('test'));
}).then(val => console.log('resolved:', val))
    .catch(error => console.log(error));
    // Error: test

如果 Promise 状态已经变成resolved,再抛出错误是无效的。

const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止,而且只会捕获最先发生的那个错误。

const p = new Promise((resolve, reject)=>{
    resolve();
});
p.then(value=>{
    console.log("hello");
    throw new Error("222");
}).then((newValue) => {
    console.log(newValue);
    throw new Error("333");
}).catch(error=>console.log(error));
// hello
// Error: 222

一般来说,不要在then()方法里面定义 reject 状态的回调函数(即then的第二个参数),总是建议 Promise 对象后面要跟catch()方法,这样可以处理 Promise 内部发生的错误。

// 不推荐
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// 推荐
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

catch() 方法返回的还是一个 Promise 对象,因此也可以链式调用。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on

(3)、Promise.prototype.finally()

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

finally 本质上是 then 方法的特例:

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

finally 方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是 rejected。这表明,finally 方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

4、Promise 构造函数自身的方法

  • Promise.any()有一个成功就返回成功。有一个 Promise 变为 fulfilled 状态,就表示成功。
  • Promise.all()有一个失败就返回失败。有一个 Promise 变为 rejected 状态,就表示失败。
  • Promise.race()第一个结果就是最终结果。返回第一个执行的 Promise 的最终状态。适用于“请求超时处理”。
  • Promise.allSettled():总是返回成功的。所有的 promise 都执行结束后,总是返回 fulfilled 状态。适用于“不关心异步操作的结果,只关心这些操作有没有结束”。
  • Promise.resolve():该方法会将普通对象转状态为 fulfilled 的 Promise 对象。
  • Promise.reject():该方法会将普通对象转状态为 rejected 的 Promise 对象。
  • Promise.try():统一管理 promise 里的同步的或异步的函数,同步的函数执行后自动返回同步的结果,异步的函数执行后自动返回异步,不用分别管理。

(1)、Promise.all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

上面代码中,p的状态由p1、p2、p3决定,分成两种情况:

  • 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  • 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

举个栗子:

// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});

上面代码中,promises是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。

如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]

上面代码中,p1会resolved,p2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。

 如果p2没有自己的catch方法,就会调用Promise.all()的catch方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了

(2)、Promise.race()

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。

下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve。

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);

上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。 

(3)、Promise.allSettled()

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。

const promises = [
  fetch('/api-1'),
  fetch('/api-2'),
  fetch('/api-3'),
];

await Promise.allSettled(promises);
removeLoadingIndicator();

该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]);

allSettledPromise.then(function (results) {
  console.log(results);
});
// [
//    { status: 'fulfilled', value: 42 },
//    { status: 'rejected', reason: -1 }
// ]

上面代码中,Promise.allSettled()的返回值allSettledPromise,状态只可能变成fulfilled。它的监听函数接收到的参数是数组results。该数组的每个成员都是一个对象,对应传入Promise.allSettled()的两个 Promise 实例。每个对象都有status属性,该属性的值只可能是字符串fulfilled或字符串rejected。fulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值。

下面是返回值用法的例子。

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);

// 过滤出成功的请求
const successfulPromises = results.filter(p => p.status === 'fulfilled');

// 过滤出失败的请求,并输出原因
const errors = results
  .filter(p => p.status === 'rejected')
  .map(p => p.reason);

有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。

(4)、Promise.any()

Promise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。该方法目前是一个第三阶段的提案 。

Promise.any()跟Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束。

const promises = [
  fetch('/endpoint-a').then(() => 'a'),
  fetch('/endpoint-b').then(() => 'b'),
  fetch('/endpoint-c').then(() => 'c'),
];
try {
  const first = await Promise.any(promises);
  console.log(first);
} catch (error) {
  console.log(error);
}

上面代码中,Promise.any()方法的参数数组包含三个 Promise 操作。其中只要有一个变成fulfilled,Promise.any()返回的 Promise 对象就变成fulfilled。如果所有三个操作都变成rejected,那么await命令就会抛出错误。

Promise.any()抛出的错误,不是一个一般的错误,而是一个 AggregateError 实例。它相当于一个数组,每个成员对应一个被rejected的操作所抛出的错误。下面是 AggregateError 的实现示例。

new AggregateError() extends Array -> AggregateError

const err = new AggregateError();
err.push(new Error("first error"));
err.push(new Error("second error"));
throw err;

捕捉错误时,如果不用try...catch结构和 await 命令,可以像下面这样写。

Promise.any(promises).then(
  (first) => {
    // Any of the promises was fulfilled.
  },
  (error) => {
    // All of the promises were rejected.
  }
);

下面是一个例子。

var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
var alsoRejected = Promise.reject(Infinity);

Promise.any([resolved, rejected, alsoRejected]).then(function (result) {
  console.log(result); // 42
});

Promise.any([rejected, alsoRejected]).catch(function (results) {
  console.log(results); // [-1, Infinity]
});

(5)、Promise.resolve()

该方法用来将现有对象转为 Promise 对象。

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

Promise.resolve方法的参数分成四种情况:

①、参数是一个 Promise 实例

如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

②、参数是一个thenable对象

thenable对象指的是具有then方法的对象,比如下面这个对象。Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。注意与 Promise.reject() 方法区分。

let thenable1 = {
    then(resolve, reject) {
        resolve(42);
    }
};

Promise.resolve(thenable1)
.then(value => {
    console.log(value);  // 42
    console.log(value === thenable1);  // false
});

上面代码中,thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出 42。

③、参数不是具有then方法的对象,或根本就不是对象

如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。

const p = Promise.resolve('Hello');

p.then(function (s){
  console.log(s)
});
// Hello

上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。 

④、不带有任何参数

Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。

所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。

const p = Promise.resolve();

p.then(function () {
  // ...
});

需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

setTimeout(function () {
  console.log('three');
}, 0);

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');

// one
// two
// three

上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是立即执行,因此最先输出。 

(6)、Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
  console.log(s)
});
// 出错了

上面代码生成一个 Promise 对象的实例p,状态为rejected,回调函数会立即执行。

Promise.reject() 方法不会像 Promise.resolve() 方法那样将接受的一个参数自动转为 Promise 对象后执行thenable对象的then方法,而是会将这个reject的参数原封不动地作为后续方法的参数。

const thenable2 = {
    then(resolve, reject) {
        reject('出错了');
    }
};

Promise.reject(thenable2)
.catch(e => {
    console.log(e);// {then: ƒ}
    console.log(e === thenable2);// true
})

(7)、Promise.try()

实际开发中,经常遇到一种情况:不知道或者不想区分,函数 f() 是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管 f() 是否包含异步操作,都用 then 方法指定下一步流程,用 catch 方法处理 f() 抛出的错误。一般就会采用下面的写法。

Promise.resolve().then(f)

上面的写法有一个缺点,就是如果 f() 是同步函数,那么它会在本轮事件循环的末尾执行。也就是说,用 Promise 包装了以后,就变成异步执行了。

那么有没有一种方法,让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 呢?回答是可以的,并且还有两种写法。

第一种写法是用async函数来写。

const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next

上面代码中,第二行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f是同步的,就会得到同步的结果;如果f是异步的,就可以用then指定下一步,就像下面的写法。

(async () => f())()
.then(...)
.catch(...)

第二种写法是使用new Promise()。

const f = () => console.log('now');
(
  () => new Promise(
    resolve => resolve(f())
  )
)();
console.log('next');
// now
// next

上面代码也是使用立即执行的匿名函数,执行new Promise()。这种情况下,同步函数也是同步执行的。

鉴于这是一个很常见的需求,所以 ES6 提供了 Promise.try() 方法替代上面的写法。

const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next

由于Promise.try为所有操作提供了统一的处理机制,所以如果想用then方法管理流程,最好都用Promise.try包装一下。这样可以更好地管理异常。

举个栗子:

function getUsername(userId) {
  return database.users.get({id: userId})
  .then(function(user) {
    return user.name;
  });
}

上面代码中,database.users.get()返回一个 Promise 对象,如果抛出异步错误,可以用catch方法捕获,就像下面这样写。

database.users.get({id: userId})
.then(...)
.catch(...)

但是database.users.get()可能还会抛出同步错误(比如数据库连接错误,具体要看实现方法),这时你就不得不用try...catch去捕获。

try {
  database.users.get({id: userId})
  .then(...)
  .catch(...)
} catch (e) {
  // ...
}

上面这样的写法就很笨拙了,这时就可以统一用promise.catch()捕获所有同步和异步的错误。

Promise.try(() => database.users.get({id: userId}))
  .then(...)
  .catch(...)

事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。

5、Promise 构造函数的应用

(1)、Promise + fetch 获取数据

export const getApiListByType = (projectName) => {
  return new Promise((resolve, reject) => {
    fetch( url,
      {
        method: 'GET',
        mode: 'cors',
        credentials: 'include'
      })
      .then(res => resolve(res.json()))
      .catch(err => {
        reject(err)
      })
  })
}

上面代码,fetch 请求成功时,在 then() 方法中调用 resolve() 方法将成功的结果抛出。执行失败时,在 catch() 方法中调用 reject() 方法将失败的结果抛出。

在外面使用此方法获取数据时:

this.getApiListByType(demoName)
    .then(res=>{
        console.log('--->res', res)
    })
    .catch(e=>{
        console.log(e)
    )

上面代码,获取数据成功时,执行 then() 方法,接收成功的结果。获取数据失败时,执行 catch() 方法,接收失败的结果。

 (2)、异步加载图片

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}

可以简写为:

const preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};

 上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。

(3)、Promise 与 Generator 函数结合使用

使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。

function getFoo () {
  return new Promise(function (resolve, reject){
    resolve('foo');
  });
}

const g = function* () {
  try {
    const foo = yield getFoo();
    console.log(foo);
  } catch (e) {
    console.log(e);
  }
};

function run (generator) {
  const it = generator();

  function go(result) {
    if (result.done) return result.value;

    return result.value.then(function (value) {
      return go(it.next(value));
    }, function (error) {
      return go(it.throw(error));
    });
  }

  go(it.next());
}

run(g);

上面代码的 Generator 函数g之中,有一个异步操作getFoo,它返回的就是一个Promise对象。函数run用来处理这个Promise对象,并调用下一个next方法。

6、Promise 构造函数的问题

Promise 的最大问题是:代码冗余。原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。那么,有没有更好的写法呢?请看 ES6 对“协程”的不完全实现的结果—— Generator 函数

7、手写 Promise 构造函数

请戳这里:https://github.com/leocoder351/my-promise

二、Generator 函数

Generator 函数是 ES6 提供的一种异步编程解决方案。

至此,JavaScript 异步编程的实现有以下 5 种方案:

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象
  • Generator 函数

Generator 函数的最大特点是:可以暂停函数执行,返回任意表达式的值。

1、Generator 函数是 ES6 对协程的实现

协程是一种程序运行的方式:多个线程互相协作,完成异步任务。

协程有点像函数,又有点像线程。它的运行流程大致如下:

  • 第一步,协程A开始执行。
  • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
  • 第三步,(一段时间后)协程B交还执行权。
  • 第四步,协程A恢复执行。

上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。

Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。

如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表达式交换控制权。

function* gen(x) {
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器)g。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针g的next方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的yield语句,上例是执行到x + 2为止。 

2、Generator 函数的语法

(1)、Generator 函数组成

Generator 有两个区分于普通函数的部分:

  • 一是,在 function 后面,函数名之前有个 * ;
  • 二是,在函数体内部使用 yield 表达式。

其中 * 用来表示函数为 Generator 函数,yield 用来定义函数内部的状态。

function* func(){

    console.log("one");
    yield '1';

    console.log("two");
    yield '2'; 

    console.log("three");
    return '3';
}

(2)、Generator 函数的执行机制

调用 Generator 函数和调用普通函数一样,在函数名后面加上 () 即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,需要调用遍历器对象Iterator 的 next 方法,指针就会从函数头部或者上一次停下来的地方开始执行。

Generator 函数是分段执行的,yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
hw.next();
// {value: "hello", done: false}
hw.next();
// {value: "world", done: false}
hw.next();
// {value: "ending", done: true}
hw.next()
// { value: undefined, done: true }

上述代码中,定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。

上述代码中,一共调用了四次 next 方法:

  • 第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。
  • 第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。
  • 第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。
  • 第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。

【总结一下】Generator 函数的执行顺序

调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

(3)、作为对象属性的 Generator 函数

如果一个对象的属性是 Generator 函数,可以简写成下面的形式。

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

// 等同于
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

(4)、Generator 函数的作用域

Generator 函数执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。

function* gen() {
  yield 1;
  return 2;
}

let g = gen();

console.log(
  g.next().value,
  g.next().value,
);

Generator 函数的实例(一个遍历器对象)能够继承 Generator 函数的 prototype 对象上的一切属性和方法。

function* g() {}

g.prototype.a= function () {
  return 11;
};

let obj = g();

obj instanceof g;// true
obj.a();// 11

上面代码表明,Generator 函数g返回的遍历器obj,是g的实例,而且继承了g.prototype。 

(5)、Generator 函数的注意事项

①、Generator 函数返回的总是遍历器对象,而不是this对象。

如果把 Generator 函数当作普通的构造函数,并不会生效,因为 Generator 函数返回的总是遍历器对象,而不是this对象。 

function* g() {
  this.a = 11;
}

let obj = g();
obj.next();
obj.a;// undefined

上面代码中,obj对象是一个遍历器对象。Generator 函数g在this对象上面添加了一个属性a,但是obj对象拿不到这个属性。

那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this?

下面是一个变通方法。首先,生成一个空对象,使用call方法绑定 Generator 函数内部的this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3

上面代码中,首先是 F 内部的this对象绑定 obj 对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次 next 方法(因为 F 内部有两个 yield 表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在 obj 对象上了,因此 obj 对象也就成了 F 的实例。

上面代码中,执行的是遍历器对象 f,但是生成的对象实例是 obj,有没有办法将这两个对象统一呢?

一个办法就是将 obj 换成 F.prototype。

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var f = F.call(F.prototype);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

②、Generator 函数不能跟new命令一起用,会报错。

function* F() {
  yield this.x = 2;
  yield this.y = 3;
}

new F()
// TypeError: F is not a constructor

下面是一个变通方法。

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

3、yield* 表达式

(1)、yield* 表达式与 yield 表达式

yield:返回一个遍历器对象。

yield* 表达式:在返回一个遍历器对象后,立即调用该遍历器对象。也就是说,在 Generator 函数内部调用另一个 Generator 函数。

function* inner() {
  yield 'hello!';
}

//不使用 yield* 表达式
function* outer1() {
  yield 'open';
  yield inner();
  yield 'close';
}

var gen = outer1()
gen.next().value;// "open"
gen.next().value;// 返回一个遍历器对象:inner {<suspended>}
gen.next().value;// "close"

// 使用 yield* 表达式
function* outer2() {
  yield 'open'
  yield* inner()
  yield 'close'
}

var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"

上面例子中,outer2使用了yield*,outer1没使用。结果就是,outer1返回一个遍历器对象,outer2返回该遍历器对象的内部值。

(2)、yield*表达式的特点

  • yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。
  • 任何数据结构只要有 Iterator 接口,就可以被yield*遍历。
  • 如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。

①、yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。

function* foo() {
  yield 'a';
  yield 'b';
}
function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}
var myBar = bar();
console.log(myBar.next());// {value: "x", done: false}
console.log(myBar.next());// {value: "a", done: false}
console.log(myBar.next());// {value: "b", done: false}
console.log(myBar.next());// {value: "y", done: false}

// 等同于
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}
var myBar = bar();
console.log(myBar.next());// {value: "x", done: false}
console.log(myBar.next());// {value: "a", done: false}
console.log(myBar.next());// {value: "b", done: false}
console.log(myBar.next());// {value: "y", done: false}

// 等同于
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}
for (let v of bar()){
  console.log(v);
}
// x
// a
// b
// y

②、任何数据结构只要有 Iterator 接口,就可以被 yield* 表达式遍历。

比如:数组原生和字符串原生都有 Iterator 接口,所以都可以被 yield* 表达式遍历。

// 数组
function* gen(){
    yield ["a", "b", "c"];
    yield* ["a", "b", "c"];
}

let f = gen();
f.next() // {value: Array(3), done: false}
f.next() // { value:"a", done:false }

// 字符串
let read = (function* () {
    yield 'hello';
    yield* 'hello';
})();

read.next().value // "hello"
read.next().value // "h"

上面代码中,对于数组,yield命令后面如果不加星号,返回的是整个数组,yield*表达式返回的是数组的第一个遍历器对象。对于字符串,yield表达式返回整个字符串,yield*表达式返回单个字符。因为字符串具有 Iterator 接口,所以被yield*遍历。

③、如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。

function* foo() {
  yield 2;
  yield 3;
  return "foo";
}

function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v: " + v);
  yield 4;
}

var it = bar();

it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}

上面代码结合扩展运算符,可以简写为:

function* foo() {
  yield 2;
  yield 3;
  return "hello";
}

function* bar(fn) {
  yield 1;
  var v = yield* fn;
  console.log("v: " + v);
  yield 4;
}

console.log([...bar(foo())]);
// v: hello
// [1, 2, 3, 4]

上面代码中,存在两次遍历。第一次是扩展运算符遍历函数bar返回的遍历器对象,第二次是yield*语句遍历函数foo返回的遍历器对象。这两次遍历的效果是叠加的,最终表现为扩展运算符遍历函数foo返回的遍历器对象。所以,最后的数据表达式得到的值等于[1, 2, 3, 4]。但是,函数foo的return语句的返回值hello,会返回给函数bar内部的v变量,因此会有终端输出。

4、Generator 函数返回的遍历器对象的方法

Generator 函数总是返回一个遍历器,该实例继承了 Generator 函数的prototype对象上的方法。

Generator 函数返回的遍历器对象的方法有三个:

  • next():将 yield 表达式替换成一个值。
  • throw():将 yield 表达式替换成一个 throw 语句。
  • return():将 yield 表达式替换成一个 return 语句。

这三个方法的作用:本质上都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。

(1)、Generator.prototype.next()

next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息(value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

next() 方法可以接收一个参数:(可选的)一个值。

当 next() 方法传入参数时,该方法用来将 yield 表达式替换成一个值。

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}

gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;

next() 方法类似于分解的 for... of 语句,用来逐步遍历 Generator 函数生产的 Iterator 对象。

一般情况下,next 方法不传入参数的时候,yield 表达式的返回值是 undefined。当 next 传入参数的时候,该参数会作为上一步yield的返回值。

function* sendParameter(){
    console.log("strat");
    var x = yield '2';
    console.log("one:" + x);
    var y = yield '3';
    console.log("two:" + y);
    console.log("total:" + (x + y));
}

// next 不传参
var sendp1 = sendParameter();
console.log(sendp1.next());
// strat
// {value: "2", done: false}
console.log(sendp1.next());
// one:undefined
// {value: "3", done: false}
console.log(sendp1.next());
// two:undefined
// total:NaN
// {value: undefined, done: true}

// next 传参
var sendp2 = sendParameter();
console.log(sendp2.next(10));
// strat
// {value: "2", done: false}
console.log(sendp2.next(20));
// one:20
// {value: "3", done: false}
console.log(sendp2.next(30));
// two:30
// total:50
// {value: undefined, done: true}

(2)、Generator.prototype.throw()

throw()是将yield表达式替换成一个throw语句。

gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

throw 方法可以在 Generator 函数体外面抛出异常,在函数体内部捕获。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('catch inner', e);
  }
};
 
var i = g();
i.next();
 
try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('catch outside', e);
}
// catch inner a
// catch outside b

(3)、Generator.prototype.return()

return()是将yield表达式替换成一个return语句。

gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;

return 方法返回给定值,并结束遍历 Generator 函数。

return 方法提供参数时,返回该参数;不提供参数时,返回 undefined 。

function* foo(){
    yield 1;
    yield 2;
    yield 3;
}
var f = foo();
f.next();
// {value: 1, done: false}
f.return("foo");
// {value: "foo", done: true}
f.next();
// {value: undefined, done: true}

5、Generator 函数的应用

(1)、异步操作的同步化表达

用 Generator 函数处理异步操作,改写回调函数。具体实现就是:把异步操作写在yield表达式里面,等到调用next方法时再往后执行。

①、通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。

function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);
  });
}

var it = main();
it.next();

上面代码的main函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined。 

②、通过 Generator 函数逐行读取文本文件。

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while(!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}

(2)、控制流管理

如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。 

step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

采用 Promise 改写上面的代码。

Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(function (value4) {
    // Do something with value4
  }, function (error) {
    // Handle any error from step1 through step4
  })
  .done();

Generator 函数可以进一步改善代码运行流程。

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

(3)、部署 Iterator 接口

    利用 Generator 函数,可以在任意对象上部署 Iterator 接口。

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7

上述代码中,myObj是一个普通对象,通过iterEntries函数,就有了 Iterator 接口。也就是说,可以在任意对象上部署next方法。

6、Generator 函数的自动执行

自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。

能够做到自动执行的机制有:回调函数、Promise 对象、Thunk 函数、co 模块等。

(1)、Thunk 函数

Thunk 函数是自动执行 Generator 函数的一种方法。

如果将参数放到一个临时函数之中,再将这个临时函数传入函数体。那么,这个临时函数就叫做 Thunk 函数。

在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

上面代码中,fs模块的readFile方法是一个多参数函数,两个参数分别为文件名和回调函数。经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做 Thunk 函数。

任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。

// ES5版本
var Thunk = function(fn){
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      return fn.apply(this, args);
    }
  };
};

// ES6版本
const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};

使用上面的转换器的例子:

function f(a, cb) {
  cb(a);
}
const ft = Thunk(f);

ft(1)(console.log) // 1

生产环境下,建议使用 Thunkify 模块

Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。

function run(fn) {
  var gen = fn();

  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
  }

  next();
}

function* g() {
  // ...
}

run(g);

上面代码的run函数,就是一个 Generator 函数的自动执行器。内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next方法),然后判断 Generator 函数是否结束(result.done属性),如果没结束,就将next函数再传入 Thunk 函数(result.value属性),否则就直接退出。

有了这个执行器,执行 Generator 函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入run函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在yield命令后面的必须是 Thunk 函数。

var g = function* (){
  var f1 = yield readFileThunk('fileA');
  var f2 = yield readFileThunk('fileB');
  // ...
  var fn = yield readFileThunk('fileN');
};

run(g);

上面代码中,函数g封装了n个异步的读取文件操作,只要执行run函数,这些操作就会自动完成。这样一来,异步操作不仅可以写得像同步操作,而且一行代码就可以执行。

Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。

(2)、co 模块

co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。

co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co 模块。

var gen = function* () {
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

var co = require('co');
co(gen);

上面代码中,Generator 函数只要传入co函数,就会自动执行。

co函数返回一个Promise对象,因此可以用then方法添加回调函数。

co(gen).then(function (){
  console.log('Generator 函数执行完成');
});

上面代码中,等到 Generator 函数执行结束,就会输出一行提示。

进一步学习 co 模块,请戳此链接:https://es6.ruanyifeng.com/#docs/generator-async#co-%E6%A8%A1%E5%9D%97

三、async 函数

深入浅出 async 和 await:javascript - 理解 JavaScript 的 async/await_个人文章 - SegmentFault 思否

1、async 函数的语法

(1)、创建一个 async 函数

创建一个 async 函数的方式有以下 5 种方法:

  • 函数声明
  • 函数表达式
  • 对象的方法
  • Class 的方法
  • 箭头函数
// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};

(2)、async 函数的调用

async 函数自带执行器。也就是说,async 函数可以像普通函数那样直接通过小括号调用。

async function helloAsync(){
    return "hello";
}
  
console.log(helloAsync());
// Promise {<resolved>: "hello"}

(3)、async 函数的 await 命令

async 函数的 await 命令后面,可以是 Promise 对象,也可以是原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

  • 如果await命令后面是一个 Promise 对象,就返回该对象的结果。
function testAwait (x) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(x);
        }, 2000);
    });
}
 
async function helloAsync() {
    return await testAwait ("hello world"); 
}
helloAsync().then(v => console.log(v));
// hello world
  • 如果await命令后面不是 Promise 对象,就直接返回对应的值。
async function f() {
    // 等同于
    // return 123;
    return await 123;
}

f().then(v => console.log(v))
// 123
  • 如果 await 命令后面是一个 thenable 对象(即定义了then方法的对象),那么 await 会将其等同于 Promise 对象。
class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  then(resolve, reject) {
    const startTime = Date.now();
    setTimeout(
      () => resolve(Date.now() - startTime),
      this.timeout
    );
  }
}

(async () => {
  const sleepTime = await new Sleep(1000);
  console.log(sleepTime);
})();
// 1000

上面代码中,await命令后面是一个Sleep对象的实例。这个实例不是 Promise 对象,但是因为定义了then方法,await会将其视为Promise处理。 

(4)、async 函数的执行

当async函数执行的时候,一旦遇到await命令就会先返回该命令对应的结果,等到所有异步操作完成,最后会返回一个 Promise 对象,之后,可以用then方法指定下一步的操作。
有三种情况,会让async函数执行结束,返回 Promise 对象:

  • 遇到 return 语句;
  • 抛出错误;
  • 其内部所有 await 命令后面的 Promise 对象执行完。

①、遇到return语句

async function f() {
    await console.log(123);;
    return "你好!";
    await console.log('666');
}

f().then(v=>console.log("v:", v));
// 123
// v: 你好!

②、抛出错误

async function f() {
    throw await new Error("出错了");
    await Promise.resolve('hello world'); // 不会执行
}
f().then(v=>console.log("v:", v));
// Uncaught (in promise) Error: 出错了

③、内部所有 await 命令后面的 Promise 对象执行完

function testAwait (x) {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(x);
            resolve(x);
        }, 2000);
    });
}
async function f() {
    await console.log(123);;
    await testAwait ("hello world");
    return "你好!";
}

f().then(v=>console.log("v:", v))
// 123
// "hello world"
// v: 你好!

(5)、async 函数的错误处理

await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到。同时,整个 async 函数都会中断执行。
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。两种优化方法:

  • 第一种是,可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。
  • 第二种是,await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

①、将 await 放在 try...catch 结构里面(推荐)

async function f() {
  try {
    throw await new Error("出错了");
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f().then(v => console.log(v))
// hello world

这样不管这个异步操作是否成功,第二个await都会执行 。

②、await 后面的 Promise 对象再跟一个 catch 方法

async function f() {
    await Promise.reject('出错了').catch(e => console.log(e));
    return await Promise.resolve('hello world');
}

f().then(v => console.log(v))
// 出错了
// hello world

(6)、注意事项

  • await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。
  • 多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
  • await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
  • async 函数可以保留运行堆栈。

①、await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。

async function f() {
  try {
    throw await new Error("出错了");
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f().then(v => console.log(v))
// hello world

②、 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

let foo = await getFoo();
let bar = await getBar();

上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

上面两种写法,getFoo和getBar都是同时触发,这样就会缩短程序的执行时间。 

③、await命令只能用在async函数之中,如果用在普通函数,就会报错。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 报错
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}

上面代码会报错,因为await用在普通函数之中了。但是,如果将forEach方法的参数改成async函数,也有问题。 

function dbFuc(db) { //这里不需要 async
  let docs = [{}, {}, {}];

  // 可能得到错误结果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}

上面代码可能不会正常工作,原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}

也可以使用数组的reduce方法。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  await docs.reduce(async (_, doc) => {
    await _;
    await db.post(doc);
  }, undefined);
}

上面例子中,reduce方法的第一个参数是async函数,导致该函数的第一个参数是前一步操作返回的 Promise 对象,所以必须使用await等待它操作结束。另外,reduce方法返回的是docs数组最后一个成员的async函数的执行结果,也是一个 Promise 对象,导致在它前面也必须加上await。

如果确实希望多个请求并发执行,可以使用Promise.all方法。

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

 ④、async 函数可以保留运行堆栈。

const a = () => {
  b().then(() => c());
};

上面代码中,函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()或c()报错,错误堆栈将不包括a()。

现在将这个例子改成async函数。

const a = async () => {
  await b();
  c();
};

上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包括a()。 

2、async 函数的实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。async 函数是 Generator 函数的语法糖。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。

下面给出spawn函数的实现,基本就是前文自动执行器的翻版。

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

3、async 函数与 Promise 对象、Generator 函数的比较

async 函数是 Generator 函数的语法糖:async函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。

async 函数对 Generator 函数的改进,体现在以下四点:

  • 内置执行器:Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数可以像普通函数那样直接简单的使用。
  • 更好的语义:async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  • 更广的适用性:co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
  • 返回值是 Promise:async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。可以用then方法指定下一步的操作。进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

举个栗子:假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

公共代码部分:

function f1(elem){

    let nowTarget, speed = 5, timer;
    clearInterval(timer);
    timer = setInterval(()=>{
        
        nowTarget = parseInt(getComputedStyle(elem)["left"]);// 初始值
        
        if(nowTarget >= 300){
            clearInterval(timer);
            return false;
        }else{
            console.log("f1");
            elem.style.left = nowTarget + speed + "px";
        }
    }, 50);
}

function f2(elem){

    let nowTarget, speed = 5, timer;
    clearInterval(timer);
    timer = setInterval(()=>{

        nowTarget = parseInt(getComputedStyle(elem)["top"]);// 初始值

        if(nowTarget >= 200){
            clearInterval(timer);
            return false;
        }else{
            console.log("f2");
            elem.style.top = nowTarget + speed + "px";
        }
    }, 50);
}

function f3(elem){

    throw new Error("这是一个错误");
}

①、Promise 的写法

var arr = [f1, f2, f3];
var div = document.getElementById("myDiv");
chainAnimationsPromise(div, arr);

function chainAnimationsPromise(elem, animations) {

    // 变量ret用来保存上一个动画的返回值
    let ret = null;

    // 新建一个空的Promise
    let p = Promise.resolve();

    // 使用then方法,添加所有动画
    for(let anim of animations) {
        p = p.then(function(val) {
            console.log(val);
            ret = val;
            return anim(elem);
        });
    }

    // 返回一个部署了错误捕捉机制的Promise
    return p.catch(function(e) {
        console.log(e);
    /* 忽略错误,继续执行 */
    }).then(function() {
        return ret;
    });
}

虽然 Promise 的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(then、catch等等),操作本身的语义反而不容易看出来。

②、Generator 函数的写法

var arr = [f1, f2, f3];
var div = document.getElementById("myDiv");
chainAnimationsGenerator(div, arr);

function chainAnimationsGenerator(elem, animations) {

    return spawn(function*() {
        let ret = null;
        try {
            for(let anim of animations) {
            ret = yield anim(elem);
            }
        } catch(e) {
            /* 忽略错误,继续执行 */
            console.log(e);
        }
        return ret;
    });

}

function spawn(genF) {
    return new Promise(function(resolve, reject) {
        const gen = genF();
        function step(nextF) {
            let next;
            try {
                next = nextF();
            } catch(e) {
                return reject(e);
            }
            if(next.done) {
                return resolve(next.value);
            }
            Promise.resolve(next.value).then(function(v) {
                step(function() { return gen.next(v); });
            }, function(e) {
                step(function() { return gen.throw(e); });
            });
        }
        step(function() { return gen.next(undefined); });
    });
}

上面代码使用 Generator 函数遍历了每个动画,语义比 Promise 写法更清晰,用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行 Generator 函数,上面代码的spawn函数就是自动执行器,它返回一个 Promise 对象,而且必须保证yield语句后面的表达式,必须返回一个 Promise。 

③、async 函数的写法

var arr = [f1, f2, f3];
var div = document.getElementById("myDiv");
chainAnimationsAsync(div, arr);

async function chainAnimationsAsync(elem, animations) {
    let ret = null;
    try {
        for(let anim of animations) {
            ret = await anim(elem);
        }
    } catch(e) {
        /* 忽略错误,继续执行 */
        console.log(e);
    }
    return ret;
}

可以看到 Async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用 Generator 写法,自动执行器需要用户自己提供。 

4、顶层 await

顶层的await命令有点像,交出代码的执行权给其他的模块加载,等异步操作完成后,再拿回执行权,继续向下执行。

四、使用实记

1、fetch 与 Promise 的使用案例

post:

fetch(`${BASE_URL}/all/api/one`, {
  method: 'POST',
  mode: 'cors',
  credentials: 'include',
  body: JSON.stringify(value)
})
  .then(res => res.json())
  .then(res => {
    console.log(res.data);
  })
  .catch((e)=>{
    message.error(e)
  })

fetch:

  const getMyList = () => {
    const url = "/my/api/list";
    fetch(url, {
      method: "GET",
      mode: "cors",
      credentials: "include",
    })
      .then(res => res.json())
      .then(res => {
        console.log(res.data);
      })
      .catch((e)=>{
        message.error(e)
      })
  };

简单的 fetch,本身就很简洁,没必要改成 async-await,否则反而显得不那么优雅了。

2、async-await 与 Promise 的使用案例

在react-redux中,我们想在处理异步接口数据时返回一个Promise对象,怎么办呢?

export const _dispatch = (dispatch, action) => {
  const { payload } = action;
  if (typeof payload !== "object") {
    console.log('_dispatch的参数类型错误:参数必须是一个对象');
  }
  return new Promise((resolve, reject) => {
    dispatch({
      type: action.type,
      payload: {
        ...action.payload,
      },
      callback: {
        resolve,
        reject,
      },
    });
  });
};

使用:

const Test = () => {

  const {
    getOneData,
    getTwoData,
    pageModel,
  } = props;

  const { oneList, twoList } = pageModel;

  useEffect(() => {
    getOneList();
    getTwoData({key: "two"});
  }, [])

  const getOneList = () => {
    getOneData({key: "one_my"});
    getOneData({key: "one_all"});
  }

  return (
    <div>
      {oneList.length ? (
        <>
          {oneList.map((item, idx) => {
            <div key={idx}>
              {item}
            </div>
          })}
        </>
      ) : ("")}
      {twoList.length ? (
        <>
          {twoList.map((item, idx) => {
            <div key={idx}>
              {item}
            </div>
          })}
        </>
      ) : ("")}
    </div>
  )

const mapStateToProps = (state) => ({
  pageModel: state.pageModel.toJS(),
});

const mapDispatchToProps = (dispatch) => {
  return {
    getOneData (payload) => {
      return _dispatch (dispatch, {
        type: "ONE_LIST",
        payload,
      })
    },
    getTwoData (payload) => {
      dispatch({
        type: "TWO_LIST",
        payload,
      })
    }
  }
}

以上述代码为例,说明一下:在 mapDispatchToProps 中使用 _dispatch 和 dispatch 的区别:

  • _dispatch:在需要返回一个 callback 时使用,传入的参数必须是一个对象,返回的是一个 Promise 对象。
  • dispatch:在不需要返回一个 callback 时使用,传入任意类型参数,返回任意类型参数。

为上述案例Test添加loading状态:

const Test = () => {

  const {
    getOneData,
    getTwoData,
    pageModel,
  } = props;

  const { oneList, twoList } = pageModel;
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    getOneList();
    getTwoData({key: "two"});
  }, [])

  const getOneList = () => {
    setLoading(true);
    getOneData({key: "one_my"})
      .then(() => getOneData({key: "one_all"})
      .then(() => {
        setLoading(false);
      }));
  }

  return (
    <div>
      {loading ? (
        <>
          {oneList.length ? (
            <>
              {oneList.map((item, idx) => {
                <div key={idx}>
                  {item}
                </div>
              })}
            </>
          ) : ("")}
          {twoList.length ? (
            <>
              {twoList.map((item, idx) => {
                <div key={idx}>
                  {item}
                </div>
              })}
            </>
          ) : ("")}
        </>
      ) : (
        没有数据
      )
    </div>
  )

const mapStateToProps = (state) => ({
  pageModel: state.pageModel.toJS(),
});

const mapDispatchToProps = (dispatch) => {
  return {
    getOneData (payload) => {
      return _dispatch (dispatch, {
        type: "ONE_LIST",
        payload,
      })
    },
    getTwoData (payload) => {
      dispatch({
        type: "TWO_LIST",
        payload,
      })
    }
  }
}

上述代码中的getOneList方法可以进一步优化:

  const getOneList = async() => {
    await setLoading(true);
    await getOneData({key: "one_my"});
    await getOneData({key: "one_all"});
    setLoading(false);
  }

什么时候可以使用 async-await ?当知道返回值是 Promise 对象的时候。

为什么不使用 then,而使用 async-await ?两个原因:

一是:then 的结构是嵌套的,没有 async-await 简洁。

二是:上述代码中,setLoading 也是异步的,使用 then 时,若要将 setLoading 也同步化执行,需要为其创建Promise对象(案例中没有实现这一步),比较麻烦;而使用 async-await,只需在其前面加上 await 关键字,就可以实现其同步化执行。

五、踩坑记

Promise、Generator 和 async 踩坑记:Promise、Generator 和 async 踩坑记_weixin79893765432...的博客-CSDN博客

推荐链接:

异步Promise及Async/Await可能最完整入门攻略

本文参考文档:

Promise 对象

Genderator 函数

Async 函数

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值