JS 异步编程:种类和原理

1 回调函数

ajax('api/user', function(err, data) {
	if (err) {/* 发生错误 */}
  ajax('api/userinfo', function(err, data) {
    ...  /* 回调地狱 */
  }
})

错误处理困难

  • 回调函数发生错误时,无法使用 try-catch 来处理错误。由于事件循环机制,回调执行和 try-catch 不会位于同一步骤中;
  • 因此,一般回调函数要手动传入 err,来处理错误,也就产生了大量样板代码

回调地狱

  • 回调套回调,执行连续步骤非常棘手

代码耦合,维护困难

2 Promise

3 Generator

我们一步步引出 generator

for...of

for...of可迭代对象上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

可迭代对象?

String, Array, TypedArray, Map and Set 是所有内置可迭代对象, 因为它们的原型对象都有一个 @@iterator 方法。

  • 可迭代协议:原型中都实现了一个方法 Symbol.iterator,返回一个对象的无参函数,被返回对象符合迭代器协议。
  • 迭代器协议:实现一个next()方法,返回{value: [any], done: [boolean]} 对象。

image-20200323120323292

iterator.next() // 输出 {value: 1, done: false}
iterator.next() // 输出 {value: 2, done: false}
iterator.next() // 输出 {value: 3, done: false}
iterator.next() // 输出 {value: undefined, done: true}
// done 了之后,value = undefined

所以, 为什么不能用 for...of 来遍历一个对象呢? 原因很简单:JavaScript 的对象中没有实现一个这样的 iterator

Generator

来看看写法:

function* gen() {
  yield 1
  yield 2
  yield 3
  yield 4
}
console.dir(gen)

image-20200323134805237

熟悉的配方,熟悉的味道~

image-20200323135021029

Generator 有什么用?

它能够中断执行代码的特性,可以帮助我们来控制异步代码的执行顺序。

例如有两个异步的函数 AB, 并且 B 的参数是 A 的返回值,也就是说,如果 A 没有执行结束,我们不能执行 B

function* effect() {
  const { param } = yield A()
  const { result } = yield B(param)
  console.table(result)
}

const iterator = effect()
iterator.next()
iterator.next() // 拿到result

执行两次 next() 得到结果,看起来很傻不是吗?有没有好的办法呢?(废话,肯定有啊) 假设你在每次执行 A() / B() 的请求结束之后,都会自动执行 next() 方法呢?

Generator + Promise

Generator作为 ES6 中使用协程的解决方案来处理异步编程的具体实现,它的特点是:

Generator 中可以使用 yield 关键字配合实例 gen 调用 next() 方法,来将其内部的语句分割执行。简言之 : next() 被调用一次,则 yield 语句被执行一句,随着 next() 调用, yield 语句被依次执行。

所以,异步编程使用 GeneratorPromise 来实现的原理是什么呢?

  1. 因为 Generator 本身 yield 语句是分离执行的,所以我们利用这一点,在 yield 语句中返回一个 Promise 对象;
  2. 首次调用 Generator 中的 next() 后, 假设返回值叫 result ,那么此时 result.value 就是我们定义在 yield 语句中的 Promise 对象。

Generator实现的核心在于上下文的保存,函数并没有真的被挂起,每一次yield,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个context对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样

4 Async/Await

Generator 的语法糖。

和 Generator 的对比:

1、Generator 出现在ES2015中,async 出现在ES2017中,async 是 Generator 的语法糖;

2、执行方式不同,Generator 执行需要使用执行器(next()等方法);async 函数自带执行器,与普通函数的执行一样;

3、async 的语法语义更加清楚,async 表示异步,await 表示等待;而 Generator 函数的(*)号和 yield 的语义就没那么直接了;

4、Generator 中 yield 后面只能跟 Thunk 函数或 Promise 对象;而 async 函数中 await 后面可以是 promise 对象或者原始类型的值(会自动转为立即resovle的promise对象);

5、返回值不同,Generator 返回遍历器,相比于 async 返回 promise 对象操作更加麻烦。