JS异步编程(1)-历史演进

异步编程诞生的原因

JavaScript 在 1992 年发布

这里致敬一下 JavaScript 主要创造者与架构师,布兰登·艾克

感谢“祖师爷”赏的饭碗

JS的单线程

JS设计的初衷是为了表单校验和 dom 操作
为了防止一个线程对dom操作时,另一个线程删除这个dom ,因此将其设计为单线程

单线程的优缺点

单线程的模式有它的好处,但同时也带来了问题,那就是阻塞

  • 同步运行:单线程意味着两段代码不能同时运行,而是必须逐步地运行
  • 阻塞操作:如果有非常耗时的任务,会出现用户长时间等待,并且在当前任务完成前,其他操作都无法响应的情况

所以在同步代码执行过程中,需要将这类耗时的任务进行异步处理
避免阻塞正常的逻辑执行

JS异步编程的核心原理

JS异步编程的核心是 Event loop
Web 端和 Node 端各有不同

在这里不是主要内容,简单描述一下

Web 端

Event loop 是由 HTML5 规范明确定义,由各大浏览器厂商各自实现的一套 JavaScript 在浏览器环境下的事件循环机制

Node端

Nodejs 的 Event loop 是基于 libuv,并且 libuv 已经对 Event loop 作出了实现

阶段一:回调函数

最基本也是最原始的异步编程模式就是回调函数

// taks1 -> cb1 -> task2 -> cb2 -> task3 -> cb3
task1(function cb1() {
    task2(function cb2() {
        task3(function cb3() {
            cb3()
        })
    })
})

回调函数给人一种什么样的感觉?像什么?
像是俄罗斯套娃,大的套小的,随着套娃越来越多。某一天,我突然想把最里面的一个拿出来,这时候就绝望了。

这说明伴随着回调函数的嵌套增加,带来了一些问题,比如:

  • 修改成本过高
    • 当我们需要修改回调函数时,发现无从下手
  • 局部作用域嵌套
    • 由于使用函数嵌套,最内层的函数拥有最大的作用域范围。
    • 极有可能不小心重定义或者修改了上层作用域的变量或函数
    • 代码混乱,不够直观

回调函数的嵌套问题有一个统称——回调地狱
为了解决回调地狱的问题,到 ES6 发布,出现了第二种异步编程模式 Promise

阶段二:Promise

Promise 是 ES6 中引入的新特性,与传统回调函数写法相比,有两个区别:

  1. Promise 使用 then 函数进行链式调用,不再是以往的那种嵌套结构了
  2. 每个 then 函数中的回调函数互相独立,不再有作用域的干扰

将之前的例子改写,可以看到代码逻辑变得更加清晰

// taks1 -> then -> task2 -> then -> task3
task1()
    .then(function() {
        task2()
    })
    .then(function() {
        task3()
    })

但是 Promise 本身还是有一堆的 then 函数,then函数中还是写了一堆的回调函数
依旧不能让我们像写同步代码一样写异步的代码,更像是一个伪同步写法

这个时候同样伴随 ES6 发布的 Generator 提供了一种思路

阶段三:Generator

Generator 也是 ES6 引入的新特性,原本是为了实现一种新的状态机制管理
为我们提供控制函数执行阶段的能力

function* task1() {
    yield task2()
    yield task3()
}
let result = task1() // task1
result.next() // task2 返回值
result.next() // task3 返回值

这段示例代码向我们展示 Generator 的几个特点

  1. 必须使用 * 来声明
  2. 使用 yield 关键字,使得函数内部写法真正像是同步任务
  3. 可中断执行,但需要手动执行next,否则后续代码不会执行

但还不够,我们看 Generator 有什么样的问题

  1. 繁琐的 next 方法调用
  2. 晦涩难懂的函数语义,单纯看 * 和 yield,谁能明白它要干嘛
  3. 用 Generator 来进行异步编程,不是开箱即用。Generator 本身和异步编程无关,但在使用过程中发现在异步编程中有巨大的价值,基本需要进行较为完善的二次封装(增加执行器),才能成为一种异步编程模式,例如 co 库
npm install co
let co = require('co')

function* task1() {
  yield task2()
  yield task3()
}
co(task1())

我们希望它能够更简单直接一点,然后 Async/Await 隆重登场了

阶段四:Async/Await

Async 是 ES8 中引入的新特性,是 Generator 的语法糖
可以近似的认为是 Generator + 执行器 + Promise 的封装

同样修改一下上面的例子

// task1 -> task2 -> task3
async task1() {
    await task2()
    await task3()
}

优点很明显:

  • 语义化清晰明确:Async 异步,Await 等待,没有歧义。其实大家也看的出来,就是把 * 和 yield 换了一下
  • 同步任务的写法:这点上也沿用了 Generator 的设计
  • 开箱即用:专门为异步编程设计,不需要像 Generator 进行二次封装(执行器)
  • Async / Await 可以嵌套使用
  • 隐式返回 Promise:可直接使用 Promise Api,进行并发异步等模式的开发

综合来看
Async/Await 是目前为止最完善的异步编程解决方案,解决了之前的痛点

总结

我们从时间线上来看 JavaScript 异步编程的演进过程

time.png

ES6 以前,无论是事件监听、发布订阅还是定时器,使用的还是原始的 Callback 方式

2015年 ES6 正式发布,同时将 Promise 和 Generator 引入标准。但是在社区,Promise 和 Generator 都早有自己的雏形,Promise 的概念出现的时间相对而言还要更早一些。
所以在这条时间线上,我把他们两个都定为 ES6 的正式发布的时间,但是 Promise 处在更早的时间节点

到了 2017 年 ES8 发布,Async 引入标准,成为最新的解决方案,异步编程带来的问题告一段落。

你可能感兴趣的:(JS异步编程(1)-历史演进)