[TOC]

JavaScript发展到现在,异步编程已经得到了极大的改进。异步编程的进化史:callback -> promise -> generator -> async + await。现在我们来根据其发展历史一一进行分析。

callback

在早期的JavaScript中,只支持定义回调函数来表明异步操作完成,它也是异步操作中最基本的一种方法。在这里我们需要知道,回调函数就是将函数作为变量看待,即在一个函数里将变量设置为函数,它不一定是异步代码,异步代码却一定是回调函数 。以下就是一个回调函数的例子:

function test(value, callback) {
    setTimeout(() => callback(value),1000)
}

test(3, (x) => console.log(`测试数据为: ${x}`))
// 测试数据为: 3  (大约在1000ms后)

上述方式可以完成了一个回调操作,但是如果异步返回值依赖于另一个异步返回值,结果又会怎样呢?让我们看看接下来的一个例子:

function test(value, success, failure) {
    setTimeout(() => {
        try {
            if (typeof value !== 'number') {
                throw 'please provide number as first argument'
            } 
            success(3*value)   // 成功回调
        } catch(e) {
            failure(e)   // 失败回调
        }
    },1000)
}

const successCallback = (x) => {
    test(x, (y) => console.log(`suceess: ${y}`))
}

const failureCallback = (e) => {
    console.log(`failure:${e}`)
}

test(3, successCallback, failureCallback)    // suceess: 27(大约100ms之后)

很显然,随着代码越复杂,回调策略就难以扩展,嵌套回调的代码也就难以维护,这就是著名的“回调地狱”。

Promise

什么是Promise?
Promise 是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。在Promise返回给调用者时,操作往往还没有完成,但 Promise 对象可以让我们操作最终完成时对其进行处理(无论成功还是失败)。
正如前面所述,Promises是一个有状态的对象,它可能处于以下三种状态:

  • 待定(pending):初始状态,既没有被兑现,也没有被拒绝。这是调用 fetch()返回Promise的状态,此时请求还在进行中。
  • 兑现(fullfilled):操作成功完成,然后调用then()处理函数。
  • 拒绝(rejected):操作失败,然后调用catch()处理函数。

异步编程 - 图1
Promise状态图
依图所示,我们知道无论落定为哪种状态都是不可逆的,只要从pending转换为fullfilled/rejected,期约的状态就不再改变。同时,我们应该了解到,期约的状态是私有的,为了避免根据读取到的期约状态以同步方式处理Promise,它不能直接通过JavaScript检测到
因为期约状态私有,所以只能在期约的执行器函数中进行内部操作。执行器函数主要有两个职责:初始化期约的异步行为和控制状态的最终转换。而一般来说切换期约状态是通过调用它的两个函数参数实现,该两个函数参数通常命名为resolve()和reject()。

let p1 = new Promise((resolve,reject) => resolve())
setTimeout(console.log,0,p1)  // Promise <resolved>

let p2 = new Promise((resolve, reject) => reject())
setTimeout(console.log,0,p2)  // Promise <rejected>

好了,说了这么多,让我们通过一个例子来详细探讨下。在以下例子中,我们通过发送一个请求,获得一个JSON文件:

const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

console.log(fetchPromise);  // Promise { <state>: "pending" }

fetchPromise
  .then( response => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    } else {
      console.log(`已收到响应:${response.status}`);
    }
    return response.json();  // 获取JSON格式的数据
  })
  .then( json => {
    console.log(json[0].name);
  });

console.log("已发送请求……");
在上述代码中,通过调用`fetch()`API将返回值赋予变量`fetchPromise`,然后将`json()`这个处理函数传递给Promise的`then()`方法进行调用。因为`json()`方法也是异步,我们必须连续调用两个异步函数。我们将一个新的`then()`处理程序传递`response.json()`返回的Promise。此处采用链式调用,从而能够避免了回调嵌套而导致的**回调地域**问题。<br />以下为完整的输出结果:

:::info Promise { : “pending” }
已发送请求……
已收到响应:200
baked beans ::: 可以看出,已发送请求……的消息在我们收到响应前就已被输出,即fetch()在请求进行时返回,这使得程序保持响应性。
在Promise中,有着几个常用的API,分别是Promise.all()Promise.race(),大家可以去详细了解下,这里就不阐述了。

generator

generator即生成器,它拥有在一个函数块内暂停和恢复代码执行的能力。

1.生成器基础

generator的形式就是一个函数,函数名称前面加一个星号()表明该为一个生成器,所以*箭头函数不能被用来定义generator函数。当我们调用生成器函数时会产生一个生成器对象,生成器对象一开始处于暂停执行(suspended)的状态。当调用next()方法时,生成器开始或恢复执行,同时返回一个done属性和value属性。如下例可见:

function* generatorFn() {
    return 'success';
}

let generatorObject = generatorFn()
console.log(generatorObject)   // generatorFn{<suspended>}
console.log(generatorObject.next())  // { value: 'success', done: true }

2.yield中断执行

yield关键字可以让生成器停止和开始执行。
生成器函数在遇到yield关键字前正常执行,遇到该关键字后执行停止,但函数作用域的状态会被保留。停止执行的生成器函数只能通过调用生成器对象的next()方法来恢复执行。yield关键字退出的generator函数处在done:false状态;return关键字退出的生成器函数处于done:true状态。

function* generatorFn() {
    yield 'foo';
    yield 'bar';
    return 'baz';
}

let generatorObject = generatorFn()
console.log(generatorObject.next())   // { value: 'foo', done: false }
console.log(generatorObject.next())   // { value: 'bar', done: false }
console.log(generatorObject.next())   // { value: 'baz', done: true }

同时,yield关键字可以作为函数的中间参数使用,该参数就会被当作上一个yield表达式的返回值,但值得注意的是第一次调用next()传入的值不会被使用,因为这一次调用时为了开始执行generator函数

function *foo(x) {
  let y = 2 * (yield (x + 1))
  let z = yield (y / 3)
  return (x + y + z)
}
let it = foo(5)
console.log(it.next())   // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}

可能结果跟你想象不一致,接下来我们逐行代码分析:

  • 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
  • 当执行第二次 next 时,传入的参数12就会被当作上一个yield表达式的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 12,所以第二个 yield 等于 2 12 / 3 = 8
  • 当执行第三次 next 时,传入的参数13就会被当作上一个yield表达式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42。

    async/await

    在学习异步函数async/await前,让我们先来看一个例子:
    let p = new Promise((resolve, reject) => setTimeout(resolve,1000,2));
    这个期约在1000ms后解决为数值3,如果我们想要访问它,则必须写一个解决处理程序:
    p.then((x) => console.log(x);
    这样极其不方便,任何需要访问该期约产生值的代码都需要以处理程序的形式来接收这个值。为了解决这样的问题,ECMAScript提供了async/await关键字。

    1.async

    async关键字用于声明异步函数,它可以用在函数声明、函数表达式、箭头函数和方法上。当异步函数中使用return关键字返回值(没有return则返回undefind)时,这个值会被Promise.resolve()包装成一个Promise对象,然后交由then()的处理程序进行‘解包’。同时,当在异步函数中抛出错误时,会返回一个拒绝期约,但拒绝期约的错误无法被函数捕获。 ```javascript async function foo() { console.log(1); return 5; }

async function qux() { console.log(2) throw 6; }

async function baz() { console.log(3); Promise.reject(7); }

foo().then(console.log); qux().catch(console.log); baz().catch(console.log); console.log(4);

// 输出结果顺序为:1 2 3 4 5 6 Uncaught (in promise):7

相信大家看了上面的例子都了解,async关键字就是一个标识符,其执行基本上与普通函数没什么区别。所以这个时候就需要await的**暂停和恢复执行**的能力了。
<a name="H50Mt"></a>
### 2.await
await关键字能暂停异步函数后面的代码,记录在哪里暂停,让出JavaScript运行时的执行线程;然后尝试‘**解包**’对象,等到await右边的值可用,JavaScript运行时就会向消息队列中推送一个任务,这个任务就会恢复异步函数的执行。<br />下面来看个例子:
```javascript
async function foo() {
  console.log(1);
  console.log(await Promise.resolve(5));  
  console.log(6);
}

async function qux() {
  console.log(2)
  await (() => {throw 7})();
}

async function baz() {
  console.log(3);
  await Promise.reject(8);
  console.log(9);   // 该行代码不会执行
}

foo().then(console.log);
qux().catch(console.log);
baz().catch(console.log);
console.log(4);

// 输出结果顺序为:1  2  3  4  5  6  7  8

值得注意一点的是,单独的Promise.reject()不会被异步函数捕捉,而在上述的baz()函数中,对拒绝期约使用await会释放错误值。同时,我们必须明确的一点是,await关键字必须在异步函数中使用,不能在顶级上下文(如