同步 & 异步
- 单线程同步编程:每个任务按照先后顺序依次执行(阻塞)
- 多线程模型:每一个任务由独立的线程控制,这些线程由操作系统管理。在一个多处理器或多核环境中可以真正实现多线程;在单处理器中,通过任务交替执行实现并发(非阻塞)
由于一些比较古老的原因,Javascript 是单线程的,也就是说在同一时刻,浏览器进程中只有一个 Javascript 的线程在执行,并且阻塞其他任务执行
为什么 Javascript 是单线程的:
- 轻量
- 简化并发模型,无死锁
- 没有线程切换开销
但是如果碰到耗时比较长的任务,比如读取文件内容,会导致后面的任务无法继续进行下去。Javascript 在后期的发展中采用异步非阻塞编程模式解决多线程并行的问题,一个异步过程可以表述如下:
- 主线程发起一个异步请求,相应的 Worker 接收请求并告知主线程已收到请求(异步函数返回)
- 主线程可以继续执行后面的代码,同时 Worker 执行异步任务
- Worker 完成工作后,通知主线程
- 主线程收到通知后,执行一定的动作(调用回调函数)
异步的实现
现在,我们定义一个函数,用来读取指定目录下的全部文件内容
1 | // 阻塞方式 |
回调函数
下面用回调函数的方式实现读取指定目录下的全部文件内容,回调函数将耗时的执行推迟到了后面执行实现了异步,没有阻塞后面函数的执行
1 | const readDirCallback = () => { |
但是,上面的回调函数仅仅嵌套了两层,假设我们要实现更为复杂的功能,那么也许就会出现 fn1(fn2(fn3(fn4(...))));
这样的情况,传说中的 ‘Callback Hell’ 就会由此产生。回调函数最大的问题是函数之间高度耦合,维护困难
优点:
- 简单
- 利于理解、部署
缺点:
- 可读性差
- 可维护性差
- 无法统一处理异常
- 无法灵活控制流程
事件监听
DOM 事件的异步方式
1 | const btn = document.querySelector('#button'); |
订阅/发布
Node.js 提供 events 模块,通过 EventEmitter
实例来触发事件,绑定在该事件上的函数被同步调用,所以需要确保事件的正确排序且避免竞争条件或逻辑错误
1 | const readPubSub = (ctx) => { |
监听器函数可以使用
setImmediate()
或process.nextTick()
方法切换到异步操作模式:
1 | const myEmitter = new MyEmitter(); |
Promise
用 Promise 实现读取指定目录下文件内容
1 | const readPromise = () => { |
与回调函数方式实现的异步相比,这段代码在视觉上清晰了许多,Promise 是典型的 Monad 风格,采用链式调用,每次产生新的 thenable 对象
但是,Promise 并没有真正消除 callback,只是利用 then()
延迟了 callback 的绑定
优点:
- 关注点分离
- 一次性
- 可以单独处理异常,也可以统一处理
缺点:
- “贪心”
- 无法取消
- 无法访问链式调用内部的值
Genertor
Generator 函数可以彻底消除 callback,看起来 Generator 与订阅/发布模式有些类似,都是通过主动调用的方式,告诉程序什么时候该去执行下一步
1 | const readGenerator = () => { |
Generator 离不开🍭co 函数库,核心代码:
1 | co(function* () { |
Async/Await
终于轮到 Async/Await 了,Async/Await 可以说是 javascript 异步编程最为优雅的一种实现方式
1 | const readAsync = async () => { |
Async/Await 看上去与 Promise 及其相似,不同的是 Async/Await 帮助我们处理了 then 链,同时,也不需要像 Generator 一样需要自己去维护内部 yield 的执行
优点:
- 代码结构清晰
- 语义化友好
- 有利于代码维护