JavaScript-异步编程

75 阅读5分钟

JavaScript是单线程的

  • 什么是单线程?

js引擎会从上到下依次执行所有js代码,在同一个单位时间js引擎只能执行一段js。当前函数没有被执行完不会执行下一个。

  • 为什么是单线程?

js最初是为了与浏览器交互而设计的,例如响应用户的点击事件、获取或修改DOM等,这些操作需要在同一时间内按照特定的顺序执行,以确保页面的正确渲染。如果JavaScript是多线程的,那么可能会出现同时修改DOM的情况,这可能会导致页面的渲染出现问题。

  • 单线程有什么问题 1.执行一段耗时的代码(网络请求、复杂计算)会阻塞后续代码执行,导致执行效率低。

如何解决单线程带来的问题?

异步编程,JavaScript引入了事件循环机制。事件循环允许JavaScript在等待异步任务完成的同时执行其他任务,从而更好地响应用户操作和处理复杂的应用逻辑。

事件循环

异步任务

在JavaScript中,异步行为主要通过以下机制来实现:

  1. 回调函数

    • setTimeout 和 setInterval
    • 网络请求(如 XMLHttpRequest 或 Fetch API)
    • 事件处理程序(例如点击事件、输入事件等)
    • 文件读写操作(FileReader API)
    • 数据库操作(IndexedDB, WebSQL)
  2. Promise

    • Promise 对象的 .then.catch.finally 方法
    • async/await(基于Promise的语法糖)
  3. MutationObserver

    • 监听DOM树的变化
  4. Message Channel

    • 在不同线程间或同一线程的不同上下文之间传递消息
  5. Web Workers

    • 创建独立于主线程运行的后台脚本,用于执行密集型计算或其他耗时操作,并通过消息传递与主线程通信
  6. Service Workers

    • 运行在网络层面,可以拦截和处理网络请求,提供离线支持,进行资源缓存等操作
  7. Async/Await

    • 使用 async 函数定义的异步操作,内部可使用 await 关键字等待Promise的结果
  8. Node.js 特有的异步API

    • I/O 操作(如 fs.readFile、fs.writeFile 等)
    • 网络请求(http、https模块)
    • DNS 查询(dns模块)
    • process.nextTick():微任务,常用于调整异步调用顺序
  9. 其他异步API

    • IndexedDB数据库操作
    • WebRTC数据传输
    • WebSocket实时通讯
    • Geolocation定位信息获取

总之,在JavaScript中,任何需要异步执行的操作,如等待网络响应、用户交互、定时任务、系统级别的I/O操作等,都会涉及上述某种形式的异步处理方式。

宏任务与微任务

宏任务(Macrotasks)

在JavaScript中,宏任务主要包括以下类别:

  1. 事件队列(Event Queue)中的任务

    • setTimeout
    • setInterval
    • setImmediate (Node.js 环境)
    • I/O 操作(如网络请求、文件读写等)
    • UI渲染(浏览器环境)
    • requestAnimationFrame
    • MessageChannel 的消息接收
    • 一些原生的UI交互事件(如点击、滚动等)

微任务(Microtasks)

微任务通常会在当前宏任务执行完毕后立即执行,且在下一个宏任务之前清空微任务队列。主要包括以下类别:

  1. Promise回调

    • .then.catch.finally 方法注册的回调函数
  2. MutationObserver 对DOM变动的回调

  3. Promise的resolve过程

  4. process.nextTick()  (Node.js 环境)

  5. Object.observe (已被废弃,但在过去曾作为微任务处理)

  6. queueMicrotask()  API(允许开发者直接将函数添加到微任务队列中)

需要注意的是,异步API随着时间推移和规范发展可能会有所变化或增加新的宏任务与微任务机制,以上罗列的是一些常见的例子。在实际开发中应以具体运行环境及规范为准。

事件循环机制

注意事项!!!

整个script代码本身就是一个宏任务。这造成了我将近两年的误解,我之前一直以为微任务一定是先于宏任务的。

image.png

如上图,当同步任务执行完毕后,就会执行所有的宏任务,宏任务执行完成后,会判断是否有可执行的微任务;如果有,则执行微任务,完成后,执行宏任务;如果没有,则执行新的宏任务,形成事件循环。

事件循环机制的整体执行流程如下

  1. 执行同步任务:JavaScript 代码从上到下逐行执行同步任务,直到遇到第一个异步任务。

  2. 检查微任务队列

    • 当同步任务执行完毕后,立即处理微任务队列中的所有任务。
    • 遇到一个微任务时(例如Promise的.then.catch回调),将其添加到微任务队列中等待执行。
  3. 执行宏任务

    • 当微任务队列为空时,执行宏任务队列中的第一个任务。
    • 执行宏任务时,如果遇到嵌套的微任务,也会将其添加到微任务队列中等待执行。
  4. 重复上述步骤

    • 不断地循环执行上述步骤,即先执行微任务队列、再执行宏任务队列,直至两个队列都为空。

总结一下,正确的流程是:

  • 同步任务 -> 微任务 -> 宏任务 -> 微任务 ... 以此类推

在每个宏任务执行完毕之后,会立即清空微任务队列,然后再执行下一个宏任务。这样确保了在一个宏任务与另一个宏任务之间,所有的微任务都能得到优先执行。