# 异步 JavaScript
JavaScript
本身是一门单线程的语言,它通过回调函数和事件循环机制,对任务进行异步处理,来实现对多任务的处理。
# 1. 进程与线程
进程与线程的一个简单解释——阮一峰 (opens new window) (文章有偏颇,可看评论加深理解)
特点 | 进程 | 线程 |
---|---|---|
本质 | 资源分配的最小单位 | 程序执行的最小单位 |
地址空间 | 进程间独立 | 同进程共享地址空间 |
成本 | 开销大 | 开销小 |
通信 | 难 | 同一个进程中各个线程之间共享同一块内存空间 |
健壮性 | 强,一个进程挂掉不影响其他进程 | 差,一个线程挂掉,整个进程就挂掉 |
# 1.1 浏览器中的多线程
浏览器内核是通过取得页面内容、整理信息(应用CSS)、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。
Chrome 浏览器为每个 Tab 页面单独开启一个进程,每个进程中包含自己的多线程。
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
- GUI 渲染线程
- JavaScript 引擎线程
- 事件触发线程
- 定时触发器线程
- 异步 http 请求线程
GUI 线程与 JS 引擎互斥(因为 JS 可以操作 DOM)
多线程的工作流程
GUI 线程
渲染页面JS 引擎线程
处理任务队列,与GUI 线程
互斥- 有事件触发时,
事件触发线程
将事件回调加入任务队列 - 定时计数器由浏览器的单独线程(
定时触发器线程
)执行(防止 JS 线程阻塞影响计数效果),计数结束后,将定时回调加入任务队列 XMLHttpRequest
由异步请求线程
执行,检测到状态变更后,将请求回调加入任务队列
# 1.2 JavaScript 中的线程
JavaScript 主线程 (opens new window) 用于浏览器处理用户事件和页面绘制等。
默认情况下,浏览器在一个线程中运行一个页面中的所有 JavaScript 脚本,以及呈现布局,回流,和垃圾回收。
这意味着一个长时间运行的 JavaScript 会阻塞线程,导致页面无法响应,造成不佳的用户体验。
# 1.3 线程间通信(Web Worker)
- 大计算量的代码,js单线程一旦遇到就会发生阻塞,所以可以把这部分代码分到另一个线程,让主线程 不至于被挂起,当然是通过
postMessage
,onmessage
进行通信。 - 可以使用 importScript(url)加载另外的脚本文件
- 可以使用setTimeout(),clearTimeout(),setInterval(), clearInterval()做一些东西。
- 可以通过XMLHttpRequest来发送请求
- 可以访问navigator,location,JSON,application等等重要的全局变量的部分属性
# 2. 事件循环
事件循环负责收集事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript
任务(宏任务),然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。
- 当主线程执行完成后,先把微任务队列清空,然后从宏任务队列取出一条任务进行执行;
- 在当前宏任务执行完成后,取出下一条宏任务之前,仍需首先把微任务队列清空;
- 循环往复,直到两个任务队列全为空。
# 2.1 宏任务与微任务
一个任务就是指计划由标准机制来执行的任何 JavaScript,如程序的初始化、事件触发的回调等。 除了使用事件
,你还可以使用 setTimeout()
或者 setInterval()
来添加任务。
区别
- 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行
- 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入
- 微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务
加入宏队列的事件
- 主执行函数
- 定时器
- ajax 回调函数
- I/O 、UI 交互事件
加入微队列的事件
- Promise 回调
- MutationObserver (opens new window) :监视对DOM树所做更改的能力
# 2.2 定时器
# setTimeout
在指定的时间后执行一段代码
# setInterval
以固定的时间间隔,重复运行一段代码
当要在页面上显示动画时,时间间隔应当设置为 16.7 ms ,以达到每秒渲染 60 帧的效果。但是,更多的帧意味着更多的处理,这通常会导致卡顿和跳跃-也称为丢帧或跳帧。
# requestAnimationFrame
setInterval()
的现代版本,旨在浏览器中高效运行动画。在浏览器下一次重新绘制显示之前执行指定的代码块,从而允许动画在适当的帧率下运行,而不管它在什么环境中运行。
它是针对
setInterval()
遇到的问题创建的,比如setInterval()
并不是针对设备优化的帧率运行,有时会丢帧。还有即使该选项卡不是活动的选项卡或动画滚出页面等问题 。
动画的平滑度直接取决于动画的帧速率,并以每秒帧数(fps
)为单位进行测量。这个数字越高,动画看起来就越平滑。
requestAnimationFrame()
总是试图尽可能接近 60 帧/秒的值,当然有时这是不可能的。如果有一个非常复杂的动画,在一个缓慢的计算机上运行它,帧速率将更少。 requestAnimationFrame()
会尽其所能利用现有资源提升帧速率。
如果动画处于屏幕外,则浏览器不会去执行动画代码。
# 3. Promise
Promise 是 JavaScript 中进行异步编程的新的解决方案。从语法上讲, Promise 是一个构造函数;从功能上说, Promise 对象用来封装一个异步操作并可以获取其结果。
优势
- 指定回调函数的方式更加灵活(比解决回调地狱更能体现个人技术深度)
- 纯回调方式必须在启动异步任务之前指定
- Promise 指定时机更加灵活,甚至可以在异步任务结束后指定
- Promise 可指定多个回调函数
- 支持链式调用
- 纯回调方式会引发回调地狱问题
# 3.1 执行器函数
一个 Promise 对象中包含执行器和异步操作,执行器的操作是同步执行的。
const promise1 = new Promise((resolve, reject) => {
// 内部执行器,是同步执行的
console.log(1)
// ……
// 异步任务
setTimeout(() => {
console.log(3)
// 状态改变
resolve()
}, 0)
})
console.log(2)
promise1.then(v => console.log(4))
// 1, 2, 3, 4
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3.2 Generator 与 async/await
纯回调函数会造成回调地狱问题:
- 回调函数嵌套调用,外部回调函数异步执行的结果是内部嵌套回调函数执行的条件
- 当有多个回调函数如此嵌套时,不便于阅读,也不便于进行异常处理
Promise
能够在一定程度上改善回调地狱的状况,但其 then
链过长时,也会产生执行流程的问题。
可以使用 Generator
生成器来使用同步的方式编写异步操作,彻底解决回调流程问题。
async/await
是 Generator
的语法糖。