JavaScript 异步编程的进化

前言

JavaScript 是一门单线程语言,即同一时间只能做一件事。但实际业务中,存在很多耗时的任务,如果按照同步任务的方式处理这些耗时操作,那么程序将在此中断,直到等待耗时任务完成才会继续向下执行,这样的体验太差了。
JavaScript 引擎对于这些耗时操作是通过异步来处理。但如何优雅组织异步代码,却是个困扰开发者多时的难题,本文主要是总结下学习异步编程的过程。

进化过程

一、嵌套回调

如果想要实现先请求 A 资源,再请求 B 资源,即后面的资源请求是依赖于前面的请求结果(串行),就需要下面的写法。

// 模拟 ajax 请求
function ajax(fn) {
  setTimeout(fn, 20);
}
function log(text) {
  console.log(text);
}
// 嵌套回调
ajax(() => {
  log("获取A资源");
  ajax(() => {
    log("获取B资源");
  })
})

可以发现,如果还需要请求 C、D、E 甚至更多相互依赖的请求,那么将形成回调地狱(callback hell)了,这是不利于开发者阅读和维护的。

二、promise

2.1 promise 是什么?

A promise represents the eventual result of an asynchronous operation。 -- Promise/A+

promise 表示一个异步操作的最终结果。

一个 Promise 是对一个异步操作的封装,promise 就像一个状态机,内部有 3 种状态:

  • pending: 挂起,正在执行
  • fulfilled(resolved):异步操作已完成,并且成功
  • rejected:异步操作失败,原因可能是发生了错误或其他理由。

当异步操作执行完成之后,promise 的状态由 pending 变成 resolved 或者 rejected,并且这种状态变化是不可逆的。

2.2 then 方法

通过 then 方法获取异步操作的结果。 then 方法接收两个匿名函数作为参数,分别代表 onResolved 和 onRejected 函数。

then 方法默认返回一个新的 promise 对象,所以可以多次调用 then 方法。

2.3 promise 链返回值

链中的 promise 能够向下一个 promise 传递数据:

  • 如果then中的回调函数返回一个值,那么then返回的Promise将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。
  • 如果then中的回调函数没有返回值,那么then返回的Promise将会成为接受状态,并且该接受状态的回调函数的参数值为 undefined。
  • 如果then中的回调函数抛出一个错误,那么then返回的Promise将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
  • 如果then中的回调函数返回一个已经是接受状态的Promise,那么then返回的Promise也会成为接受状态,并且将那个Promise的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。
  • 如果then中的回调函数返回一个已经是拒绝状态的Promise,那么then返回的Promise也会成为拒绝状态,并且将那个Promise的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。
  • 如果then中的回调函数返回一个未定状态(pending)的Promise,那么then返回Promise的状态也是未定的,并且它的终态与那个Promise的终态相同;同时,它变为终态时调用的回调函数参数与那个Promise变为终态时的回调函数的参数是相同的。

通过 promise 的链式操作完成串行请求:

// 模拟 ajax 请求
function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, 20);
  })
}
function log(text) {
  console.log(text);
}
// 串行请求
ajax().then(() => {
  log("获取A资源");
  return ajax()
}).then(() => {
  log("获取B资源");
}).catch((err) => {
  console.log(err);
})

三、generator

迭代器

为什么有迭代器的出现?
迭代器的出现是为了解决 for 循环的问题:嵌套增加复杂度,追踪多个变量。

迭代器是什么?
迭代器是带有特殊接口的对象。
迭代器带有 next() 方法,并返回一个包含两个属性的结果对象。

  • value: 代表下一个位置的值
  • done:代表是否有更多迭代,没有更多迭代时为 true。

生成器

Generator 本质上是一个函数,它的最大特点就是可以被中断,然后恢复执行。

和普通函数的区别:

  • 在 function 关键字和方法名中间有个星号 (*)
  • 方法体中使用 "yield" 关键字
基础示例

执行顺序:

  1. 调用 say() 函数后,该函数并未立即执行,函数的返回值是一个迭代器。
  2. 依次调用迭代器的 next() 方法

3.1 关键字说明
* 表示该函数是个生成器。
yield 关键字指定迭代器调用 next() 时按顺序返回的值。

yield 关键字本身不产生返回值。即:next() 返回的是 yield 关键字后面的表达式的值。
next 可以接受参数代表可以从外部传一个值到 Generator 函数内部。

3.2 可迭代类型
可迭代类型指那些包含 Symbol.iterator 属性的对象。
在 ES6 中,所有集合对象 (数组、set、map) 与字符串都是可迭代类型。

3.3 用 Generator 组织异步方法
Generator 函数处理异步操作的核心思想:先将函数暂停在某处,然后拿到异步操作的结果,再把这个结果传到方法体内。

// 模拟 ajax 请求
function ajax(val) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`请求${val}资源`);
      resolve(val);
    }, 20);
  })
}
function *Generator() {
  let first = yield ajax('A');
  let two = yield ajax(first);
}
const gen = Generator();
const gen1 = gen.next();
console.log('第1次执行next()',gen1);
gen1.value.then((res1) => {
  const gen2 = gen.next('B');
  console.log('第2次执行next()',gen2);
  gen2.value.then(res2 => {
    console.log('第3次执行next()',gen.next());
  })
})

在上面的例子中,通过手工一步一步地调用 next() 方法,完成了两次资源请求,下面将介绍如何自动执行 next() 。

3.4 自动执行器/任务运行器

简单的任务运行器:

function run(gen) {
  let task = gen();
  let result = task.next();
  function step() {
    if (!result.done) {
      result = task.next();
      step();
    }
  }
  step();
}

基于 Promise 的执行器:(yield 后面返回的是 Promise)

function run(gen) {
  let task = gen();
  let result = task.next();
  function step() {
    if (!result.done) {
      result.value.then((data) => {
        result = task.next();
        step(data);
      })
    } else {
      return result.value;
    }
  }
  step();
}

运行测试:

// 模拟 ajax 请求
function ajax(val) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`请求${val}资源`);
      resolve(val);
    }, 20);
  })
}
function *Generator() {
  let first = yield ajax('A');
  let two = yield ajax('B');
}
run(Generator);

3.5 生成器的 return() 方法

return() 方法后面的 next 不会执行

四、async/await

async 函数返回一个 Promise 对象,如果 return 关键字后面不是一个 Promise,那么默认调用 promise.resolve 方法进行转换。

async 函数的执行过程

  • 在 async 函数开始执行时,会自动生成一个 Promise 对象。
  • 当方法体开始执行后,如果遇到 return 关键字或者 throw 关键字,执行会立刻退出,如果遇到 await 关键字则会暂停执行,异步操作结束后,恢复执行。
  • 执行完毕,返回一个 Promise

参考

developer.mozilla Promise.then()
2.6. 专栏: 每次调用then都会返回一个新创建的promise对象
Promise 与异步编程
迭代器与生成器

你可能感兴趣的:(JavaScript 异步编程的进化)