Q: 讲一下 JS 的异步
以下都是我个人的分析和回答, 如有纰漏请大家多多指出
分析
概念区分
我们首先需要明确: JS 代码是自上而下地, 以单线程执行
所谓自上而下, 就好比多米诺骨牌, 从起点开始, 上一块骨牌倒下带动下一块, 一块一块地接力, 直到起点和终点连通. 我们将这些能够以相同速度倒下, 前后顺畅接力的多米诺骨牌, 称为是彼此同步的
但骨牌序列中可能存在倒下速度更慢的骨牌. 甚至有些骨牌如果没有外力帮它一把, 它就根本倒不下来. 我们将这种更加"笨重"的多米诺骨牌称为是异步的
当一系列彼此同步的骨牌在依次倒下的过程中突然碰到一块异步的骨牌. 此时整条骨牌线路就陷入了"迟缓"甚至是"停滞"状态:
更要命的是 JS 以单线程执行, 即从起点到终点只有一列多米诺骨牌. 想要实现起点到终点的高效连通, 就只能在这一列骨牌上下功夫
JS 对异步的处理
上述概念可以对应为如下 JS 代码:
// count 进行一次加和操作
let count = 1 + 2;
// 上一步加和操作的结果能够直接传递到下一步打印操作
// console 栏将显示 3
console.log(count);
// 需要利用网络的"请求远程资源"相较于运行在本地的"打印资源信息"显然是更慢的
let photo = downloadPhoto("http://coolcats.com/cat.gif");
// console 栏将显示 undefined, 即"打印资源信息"这一操作并没有被"成功"执行
// 因为网络请求尚未完成, "请求远程资源" -> "打印资源信息"这一组前后操作陷入了"停滞"
console.log(photo);
可为什么 console 栏会出现 undefined
? 即明明"请求远程资源"这一操作还未完成, 在其之后的"打印"操作却先执行了? 这里就涉及到 JS 对异步操作的处理:
- 当执行到异步操作时, JS 并不会等待异步操作完成, 而是将等待异步操作结果这一工作交给第三方, 自身继续执行之后的同步操作(非阻塞)
- 当异步操作完成, 第三方将告知 JS 并传递异步操作结果
- JS 依据异步操作的结果进行对应处理
这就好比当多米诺骨牌序列运行到异步骨牌时, 该异步骨牌被从序列中拿了出来, 独立地进行倒下过程. 同时, 该异步骨牌之后的同步骨牌继续运行. 等到异步骨牌完成倒下后, 又被放回到原先所在多米诺骨牌序列中. 这就在保证起点和终点连通的同时, 提高了多米诺骨牌序列的运行效率
回答
之所以存在 JS 异步, 是因为在使用 JS 的过程中, 有一部分操作会比其他操作更"慢", 例如"利用网络请求远程资源", 我们将此类操作称为异步操作. 与此同时, JS 以单线程, 自上而下地执行. 如果执行到异步操作时, 也选择和其他操作一样, 等待这一操作执行完成后再执行下一操作. 整个代码的执行就会变得缓慢甚至是停滞
为了应对该问题, JS 对异步操作采取了"非阻塞"的处理: 当执行到异步操作时, JS 会将等待异步操作结果这一工作交给第三方, 自身继续执行之后的同步操作; 当异步操作完成, 第三方将告知 JS 并传递异步操作结果; JS 依据异步操作的结果进行对应处理. 这就在保证所有代码逻辑得到执行的前提下, 提高了代码执行的效率
JS 异步是一个既庞大又复杂的体系. 上面虽然说了一大堆, 但也只是分析了 JS 异步中最基础的概念问题, 在面试中几乎是一定要继续深入的, 比如:
- JS 异步操作语法为什么要从
asyncOperation(callback)
发展为promise/then
? 它们之间有什么异同? 而promise/then
与async/await
之间又是什么关系? - JS 将等待异步操作结果这一逻辑到底交给了谁? 进而引出浏览器或是 Node, 然后引出事件循环, 再分别深入到浏览器渲染或是 Node 基础结构