JavaScript异步发展历史

742 阅读6分钟

"小和山的菜鸟们",为前端开发者提供技术相关资讯以及系列基础文章。为更好的用户体验,请您移至我们官网小和山的菜鸟们 (xhs-rookies.com/) 进行学习,及时获取最新文章。

前言

在 MDN 的 JavaScript 系列中我们已经学习了 callback、promise、generator、async/await。而在这一篇文章中,作者将以实际样例阐述异步发展历史,介绍每种实现方式的优势与不足,以期帮助读者熟悉历史进程并把握异步发展的脉络。 异步发展历史

异步

几十年前的导航网站,清爽又简单,没有什么特别的功能,只是单纯的展示,现成的网页在服务器上静静躺着,高效毫无压力,让人很喜欢。

几十年后的今天,静态页面远不能满足用户的需求,网站变得复杂起来,用户交互越来越频繁,从而产生大量复杂的内部交互,为了解决这种复杂,出现了各种系统“模式”,从而很容易的在外部获取数据,并实时展示给用户。

获取外部数据实际上就是“网络调用”,这个时候“异步”这个词汇出现了。

异步指两个或两个以上的对象或事件不同时存在或发生(或多个相关事物的发生无需等待其前一事物的完成)

image-20210116210456133

异步 callbacks

异步 callbacks其实就是函数,只不过是作为参数传递给那些在后台执行的其他函数。当那些后台运行的代码结束,就调用 callbacks 函数,通知你工作已经完成,或者其他有趣的事情发生了。

场景

let readFile = (path, callBack) => {
  setTimeout(function () {
    callBack(path)
  }, 1000)
}

readFile('first', function () {
  console.log('first readFile success')
  readFile('second', function () {
    console.log('second readFile success')
    readFile('third', function () {
      console.log('third readFile success')
      readFile('fourth', function () {
        console.log('fourth readFile success')
        readFile('fifth', function () {
          console.log('fifth readFile success')
        })
      })
    })
  })
})

优点:

  • 解决了同步问题(即解决了一个任务时间长时,后面的任务排队,耗时太久,使程序的执行变慢问题)

缺点:

  • 回调地狱(多层级嵌套),会导致逻辑混乱,耦合性高,改动一处就会导致全部变动,嵌套多时,错误处理复杂
  • 不能使用 try...catch 来抓取错误
  • 不能 return
  • 可读性差

Promise

一个Promise对象代表一个在这个 promise 被创建出来时不一定已知的值。它让您能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。 这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。

场景

let readFile = (path) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!path) {
        reject('error!!!')
      } else {
        console.log(path + ' readFile success')
        resolve()
      }
    }, 1000)
  })
}

readFile('first')
  .then(() => readFile('second'))
  .then(() => readFile('third'))
  .then(() => readFile('fourth'))
  .then(() => readFile('fifth'))

优点:

  • 状态改变后,就不会再变,任何时候都可以得到这个结果
  • 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
  • 一定程度上解决了回调地狱的可读性问题

缺点:

  • 无法取消 promise
  • 当处于 pending 状态时,无法得知目前进展到哪一个阶段
  • 代码冗余,有一堆任务时也会使语义不清晰

Generator

Generator函数是 ES6 中提供的一种异步编程解决方案。语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态,需要使用next()函数来继续执行下面的代码。

特征

  • function 与函数名之间带有(*)
  • 函数体内部使用 yield 表达式,函数执行遇到 yield 就返回

场景

var readFile = function (name, ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(name + '读完了')
      resolve()
    }, ms)
  })
}

var gen = function* () {
  console.log('指定generator')
  yield readFile('first', 1000)
  yield readFile('second', 2000)
  yield readFile('third', 3000)
  yield readFile('forth', 4000)
  yield readFile('fifth', 5000)
  return '完成了'
}
var g = gen()
var result = g.next()
result.value
  .then(() => {
    g.next()
  })
  .then(() => {
    g.next()
  })
  .then(() => {
    g.next()
  })
  .then(() => {
    g.next()
  })

优点:

  • 可以控制函数的执行,可以配合 co 函数库使用

缺点:

  • 流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)

async 和 await

async functionsawait 关键字是最近添加到 JavaScript 语言里面的。它们是 ECMAScript 2017 JavaScript 版的一部分(参见ECMAScript Next support in Mozilla)。简单来说,它们是基于 promises 的语法糖,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码,因此它们非常值得学习。

image-20210116211018846

场景 1

var readFile = function (name, ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(name + '读完了')
      resolve()
    }, ms)
  })
}

async function useAsyncAwait() {
  await readFile('first', 1000)
  await readFile('second', 2000)
  await readFile('third', 3000)
  await readFile('forth', 4000)
  await readFile('fifth', 5000)
  console.log('async文件阅读完毕')
}
useAsyncAwait()

优点

  • 内置执行器。意味着不需要像 generator 一样调用 next 函数或 co 模块

  • 更广的适用性。async 和 await 后面跟的都是 promise 函数,原始数据类型会被转为 promise

  • 语义更清晰、简洁

    缺点

  • 大量的 await 代码会阻塞(程序并不会等在原地,而是继续事件循环,等到响应后继续往下走)程序运行,每个 await 都会等待前一个完成

场景 2 场景 1 中的代码,其实 second,third 的伪请求其实并不依赖于 first,second 的结果,但它们必须等待前一个的完成才能继续,而我们想要的是它们同时进行,所以正确的操作应该是这样的。

async function useAsyncAwait() {
  const first = readFile('first', 1000)
  const second = readFile('second', 2000)
  const third = readFile('third', 3000)
  const forth = readFile('forth', 4000)
  const fifth = readFile('fifth', 5000)
  console.log('async文件阅读完毕')

  await first
  await second
  await third
  await forth
  await fifth
}
useAsyncAwait()

在这里,我们将三个 promise 对象存储在变量中,这样可以同时启动它们关联的进程。

总结

在这篇文章中,我们已经介绍了 JavaScript 异步发展史中 --- callback、promise、generator、async/await 的使用方式、优点与缺点。

发展史优点缺点
callback解决了同步问题回调地狱、可读性差、无法 try / catch 、无法 return
promise一定程度上解决了回调地狱的可读性无法取消、任务多时,同样存在语义不清晰
generator可以控制函数的执行,可以配合 co 函数库使用流程管理却不方便(即何时执行第一阶段、何时执行第二阶段
async/await语义更清晰、简洁,内置执行器认知不清晰可能会造成大量 await 阻塞(程序并不会等在原地,而是继续事件循环,等到响应后继续往下走)情况

而在现有的异步解决方案中,async/await 是使用人数最多的,它带给我们最大的好处即同步代码的风格,语义简洁、清晰。

相关文章