这是我参与更文挑战的第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 中引入的新特性,与传统回调函数写法相比,有两个区别:
- Promise 使用 then 函数进行链式调用,不再是以往的那种嵌套结构了
- 每个 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 的几个特点
- 必须使用 * 来声明
- 使用 yield 关键字,使得函数内部写法真正像是同步任务
- 可中断执行,但需要手动执行next,否则后续代码不会执行
但还不够,我们看 Generator 有什么样的问题
- 繁琐的 next 方法调用
- 晦涩难懂的函数语义,单纯看 * 和 yield,谁能明白它要干嘛
- 用 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 异步编程的演进过程
ES6 以前,无论是事件监听、发布订阅还是定时器,使用的还是原始的 Callback 方式
2015年 ES6 正式发布,同时将 Promise 和 Generator 引入标准。但是在社区,Promise 和 Generator 都早有自己的雏形,Promise 的概念出现的时间相对而言还要更早一些。
所以在这条时间线上,我把他们两个都定为 ES6 的正式发布的时间,但是 Promise 处在更早的时间节点
到了 2017 年 ES8 发布,Async 引入标准,成为最新的解决方案,异步编程带来的问题告一段落。