一文带你掌握JavaScript中的EventLoop机制

2024-03-01 0 454
目录
  • EventLoop
  • 关键点
  • 为什么 javascript 是单线程的
  • 什么是非阻塞
  • 为什么需要非阻塞
  • 实现非阻塞的方式有哪些?
    • 1) 回调函数(Callbacks)
    • 2) Promise
    • 3)async/await
  • 同步任务、异步任务、宏任务、微任务之间的概念和关系
    • 一次事件循环的执行
      • 实践
        • 总结

          EventLoop

          JavaScript是 单线程、非阻塞 的,它通过事件队列 (Event Loop) 的方式来实现异步回调。

          关键点

          • 单线程:JavaScript是单线程的,即同一个时间只能做一件事,事件循环使其能够高效地处理异步操作。
          • 非阻塞式I/O:事件循环使得JavaScript可以执行长时间运行的任务(如I/O操作),而不会阻塞线程。
          • 微任务优先:在每个宏任务(Macrotask)执行完毕后,立即执行当前 微任务队列 中的所有微任务(依次执行),在进行下一个宏任务之前。

          为什么 javascript 是单线程的

          我们知道多线程可以提高效率啊,为什么JavaScript被设计为单线程,主要原因是它创建的初衷:与用户的交互以及操作DOM。

          在Web页面中,脚本需要响应用户的操作:如点击按钮、提交表单等,这些操作涉及到对DOM的读写。假如JavaScript是多线程的,那么就会引入复杂的同步问题。

          例如:两个线程同时尝试修改同一个DOM节点,那么会产生竞态条件,导致不可预测的结果。所以js一诞生就是单线程并且未来也不会改变

          为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

          简化开发

          单线程模型简化了JavaScript的设计和使用。开发者可以更专注于实现功能,而不用担心如线程同步、死锁等多线程编程中常见的问题。

          避免DOM操作冲突

          javascript选择只用一个主线程来执行代码,任何时刻只有一个操作可以被执行,从而保证了DOM操作的一致性和可预测性。

          事件循环和异步编程

          尽管JavaScript是单线程的,但它通过事件循环(Event Loop)机制支持异步编程。事件循环允许JavaScript在执行I/O密集型或耗时任务(如Ajax请求、文件操作等)时,不会阻塞主线程。

          这是通过将这些任务设置为异步操作并配合回调函数、Promise或async/await来实现的。事件循环和异步编程模型使JavaScript能够高效地处理多种操作,而无需引入多线程的复杂性。

          什么是非阻塞

          JavaScript的非阻塞特性是指在执行耗时操作(如I/O操作、请求数据等)时,不会阻塞程序的其他部分继续执行。

          这种特性是通过异步编程模式实现的,它允许JavaScript在等待某个操作完成的同时,继续执行代码的其他部分,从而提高程序的整体性能和响应能力。

          为什么需要非阻塞

          在浏览器环境中,JavaScript运行在单线程中,这意味着在同一时间内只能执行一个任务。如果JavaScript执行一个长时间运行的任务,如从服务器下载大量数据,它将阻塞后续代码的执行,导致整个页面无法响应用户操作,甚至出现卡顿。

          通过非阻塞异步编程,JavaScript可以在等待某个长时间运行的任务完成的同时,继续执行其他任务,提高应用的响应性和用户体验。

          实现非阻塞的方式有哪些?

          1) 回调函数(Callbacks)

          最初,JavaScript通过回调函数实现非阻塞行为。当一个异步操作开始时,会传入一个函数(回调函数),这个函数会在异步操作完成时被调用。

          这种方式虽然解决了非阻塞的问题,但当有多个异步操作需要协同工作时,会导致所谓的“回调地狱”(Callback Hell),使得代码难以阅读和维护。

          2) Promise

          为了解决回调函数带来的问题,ES6引入了Promise对象。Promise提供了一种更优雅的方式来处理异步操作。

          它代表了一个异步操作的最终完成(或失败)及其结果值。通过.then()和.catch()方法,可以更容易地组织和管理异步操作及其结果。

          3)async/await

          ES7进一步引入了async和await关键字,使得使用Promise的代码可以像写同步代码那样简洁明了。

          async函数声明一个函数是异步的,而await关键字用于等待一个Promise解决(resolve)。使用async和await可以以同步的方式写出清晰、易读的代码,同时保持异步操作的非阻塞特性。

          同步任务、异步任务、宏任务、微任务之间的概念和关系

          JS 分为同步任务和异步任务;同步任务都在JS引擎线程上执行,形成一个执行栈,它们与事件循环无关;异步任务被分为宏任务和微任务,它们都依赖事件循环进行调度,但执行时机不同;

          事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到任务队列中。

          同步任务:这些任务在主线程上按顺序执行,执行过程会阻塞后续任务的执行,直到当前任务完成。同步任务的执行不依赖事件循环,它们直接在调用栈(执行栈)中按顺序执行。

          异步任务:异步任务的执行不会立即完成,它们依赖事件循环进行调度。异步任务包括宏任务和微任务,它们在特定的时间点被推入各自的队列中等待执行。

          宏任务(Macrotasks)

          宏任务是一类异步任务,它们代表了一些较大的、独立的工作单元。每个宏任务的执行会在一个新的事件循环中进行,包括:setTimeout、setInterval、I/O 操作、UI 渲染(在浏览器环境中)、postMessage等

          微任务(Microtasks)

          微任务也是异步任务,但它们用于处理一些需要尽快执行的较小的工作单元。**微任务在当前宏任务执行完毕后、下一个宏任务开始之前执行。包括:Promise(⚠️promise本身是同步的,回调函数才是异步的)的回调(.then、.catch、.finally)、MutationObserver的回调、queueMicrotask。

          一次事件循环的执行

          • 执行当前宏任务:事件循环首先从宏任务队列(也称为任务队列)中取出第一个任务执行。这包括了诸如setTimeout、setInterval、I/O 操作、用户交互事件(如点击或键盘事件)等任务。
          • 执行所有微任务:当前宏任务执行完毕后,事件循环会检查微任务队列。如果队列中有微任务(例如,由Promise.then()或MutationObserver等产生的任务),事件循环会依次执行队列中的所有微任务,直到微任务队列清空。微任务的执行是连续的,不会中断。
          • 渲染UI(如果需要):在浏览器环境中,一旦微任务队列清空,浏览器会检查是否需要执行UI渲染。通常,浏览器的UI渲染会在执行完所有微任务之后,下一个宏任务开始之前进行。
          • 继续下一个宏任务:完成当前宏任务、所有微任务以及可能的UI渲染之后,事件循环会回到第一步,从宏任务队列中取出下一个任务,开始新一轮的执行。

          在同一次事件循环中,微任务(Microtasks)总是在当前宏任务(Macrotasks)之后执行。

          一文带你掌握JavaScript中的EventLoop机制

          上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。

          只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

          实践

          setTimeout(() => {
          console.log(1)
          }, 0)
          console.log(2)
          const promise2 = new Promise((resolve) => {
          console.log(3)
          resolve(3)
          })
          promise2.then((res) => {
          console.log(4)
          })

          执行步骤和输出结果的解释:

          • setTimeout(() => { console.log(1) }, 0) 注册了一个宏任务,但它的回调函数(打印1)不会立即执行。放入宏任务队列中,等待当前执行栈清空和所有微任务完成后再执行。
          • console.log(2)是同步代码,会立即执行,输出2。
          • 创建了一个新的Promise对象promise2,*console.log(3)*也是同步代码,因此会立即执行,输出3。
          • 在Promise的executor函数中,调用resolve(3)。这会将 promise2.then((res) => { console.log(4) }) 中的回调函数加入到微任务队列,等待当前执行栈清空后执行。
          • 此时,同步代码执行完毕。
          • 在进入下一个宏任务之前,事件循环会处理所有微任务。因此,promise2.then()的回调函数会被执行,输出4。
          • 最后,事件循环处理下一个宏任务,即 setTimeout() 的回调函数,执行并输出1。

          所以,输出结果为:2、3、4、1。

          js整体执行顺序是:同步任务 -> 微任务 -> 宏任务 -> 微任务 -> 宏任务 -> …

          总结

          • 执行一个宏任务(栈中没有就从事件队列中获取)
          • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
          • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
          • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
          • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

          一文带你掌握JavaScript中的EventLoop机制

          分享一个实用工具:Event Loop可视化面板

          到此这篇关于一文带你掌握JavaScript中的EventLoop机制的文章就介绍到这了,更多相关JavaScript EventLoop机制内容请搜索悠久资源网以前的文章或继续浏览下面的相关文章希望大家以后多多支持悠久资源网!

          您可能感兴趣的文章:

          • JavaScript中EventLoop介绍
          • 前端js中的事件循环eventloop机制详解
          • JS的执行机制(EventLoop、宏任务和微任务)

          收藏 (0) 打赏

          感谢您的支持,我会继续努力的!

          打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
          点赞 (0)

          悠久资源 JavaScript 一文带你掌握JavaScript中的EventLoop机制 https://www.u-9.cn/biancheng/javascript/182533.html

          常见问题

          相关文章

          发表评论
          暂无评论
          官方客服团队

          为您解决烦忧 - 24小时在线 专业服务