JavaScript是 单线程
、非阻塞
的,它通过事件队列 (Event Loop)
的方式来实现异步回调。
我们知道多线程可以提高效率啊,为什么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可以在等待某个长时间运行的任务完成的同时,继续执行其他任务,提高应用的响应性和用户体验。
最初,JavaScript通过回调函数实现非阻塞行为。当一个异步操作开始时,会传入一个函数(回调函数),这个函数会在异步操作完成时被调用。
这种方式虽然解决了非阻塞的问题,但当有多个异步操作需要协同工作时,会导致所谓的“回调地狱”(Callback Hell),使得代码难以阅读和维护。
为了解决回调函数带来的问题,ES6引入了Promise对象。Promise提供了一种更优雅的方式来处理异步操作。
它代表了一个异步操作的最终完成(或失败)及其结果值。通过.then()和.catch()方法,可以更容易地组织和管理异步操作及其结果。
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
等产生的任务),事件循环会依次执行队列中的所有微任务,直到微任务队列清空。微任务的执行是连续的,不会中断。在同一次事件循环中,微任务(Microtasks)总是在当前宏任务(Macrotasks)之后执行。
上图中,主线程运行的时候,产生堆(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) })
执行步骤和输出结果的解释:
1
)不会立即执行。放入宏任务队列中,等待当前执行栈清空和所有微任务完成后再执行。2
。3
。4
。1
。所以,输出结果为:2
、3
、4
、1
。
js整体执行顺序是:同步任务 -> 微任务 -> 宏任务 -> 微任务 -> 宏任务 -> ...
宏任务
(栈中没有就从事件队列
中获取)微任务
,就将它添加到微任务
的任务队列中宏任务
执行完毕后,立即执行当前微任务队列
中的所有微任务
(依次执行)宏任务
执行完毕,开始检查渲染,然后GUI线程
接管渲染JS线程
继续接管,开始下一个宏任务
(从事件队列中获取)分享一个实用工具:Event Loop可视化面板
到此这篇关于一文带你掌握JavaScript中的EventLoop机制的文章就介绍到这了,更多相关JavaScript EventLoop机制内容请搜索插件窝以前的文章或继续浏览下面的相关文章希望大家以后多多支持插件窝!