前言
上篇文章中介绍了多进程的浏览器基本架构,现在,我们来谈谈单线程的 JS 代码、消息队列、事件循环、微任务和宏任务。
单线程的 JavaScript
什么是单线程 js?
如果你已经仔细阅读过上一篇文章,那么答案是显而易见的:由于浏览器是由渲染进程的主线程来执行 js 代码的,换句话说,js的运行位置是渲染进程的主线程,所以 js 自然而然就是单线程的。
js 为什么设计成单线程的?
这个问题的答案同样在上一篇文章中有所体现。浏览器中的js执行和页面渲染是在同一个线程中发生的,主线程在解析HTML生成DOM树的过程中,如果遇到<script>
标签会先执行js代码而阻塞对HTML的解析,因为 js 能够修改DOM进而影响渲染结果。但如果 js 可以拥有多个线程来执行,那么会出现一边解析HTML进行渲染,一边执行的 js 代码操作 DOM ,这样会影响到页面最终渲染效果的一致性(可预见性)。
同步任务和异步任务
- 同步任务:按顺序执行的js代码,上一个任务结束才能执行下一个任务,主线程中只执行同步任务。
- 异步任务:不进入主线程执行,而是由宿主环境提供的线程执行。当异步任务完成时,会在消息队列中添加异步任务的回调函数。
消息队列
此时,你可能会有疑问:既然 JS 是单线程的,而异步任务又不是在主线程中执行的,这不是矛盾了吗?实际上,JS的确是单线程,但他的宿主环境(浏览器,Node.js)可不是单线程的,js中一些耗时的任务,可以交由宿主环境的其他线程来执行,但这与多线程语言可以开启多个线程并行执行任务并不相同。
让我们来看看异步任务执行时发生了什么。假设js代码发出了一个异步 http 请求,此时由IO线程来接管执行http请求的代码,主线程将异步任务挂起,并继续执行接下来的同步代码,当IO线程接收到了服务器发来的响应,便将异步任务的回调加入到消息队列的队尾。
消息队列(任务队列)是在主线程之外的数据结构,每当有异步任务完成,那么他的回调函数(callback)就会被push到消息队列的队尾。主线程中所有同步任务执行完之后,由事件循环来通知主线程开始执行消息队列中的任务。
事件循环(Event Loop)
简单的说,事件循环起到通知主线程该执行异步任务回调的作用。每当主线程的同步代码执行完毕后,浏览器变开始进行事件循环,让消息队列的中的队首元素出队,并在主线程中执行,执行完成后,事件循环再次查询消息队列中的队首元素......
事件循环和消息队列相互配合,管理异步任务和它的回调函数。
微任务和宏任务
确切地说,消息队列中的元素是一个一个地宏任务,而宏任务内部有一个微任务队列。js主线程是第一个宏任务。
-
常见的微任务有:
Promise.then()
,await 发出的消息,Object.observe
、process.nextTick
-
常见的宏任务有:主线程所有同步任务、setTimeout的回调、setInterval的回调、setImmediate的回调
w3c规定:setTimeout() 有一个默认的最小延时时间为4ms,所以即使参数为0,那也是4ms后会将回调函数添加到消息队列
每当一个宏任务的主要任务完成后,事件循环便开始捕获其微任务队列中的微任务,当这个宏任务内的微任务队列为空时,事件循环才开始捕获下一个宏任务和它的微任务队列.....
为什么要分宏任务和微任务?这样的设计是为了给紧急任务一个“插队”的机会,否则新进队列的任务永远放在队尾。可以把微任务理解为更加着急执行的任务,所以可以“插队”,排在宏任务之前被事件循环捕捉。
下面用一组图片来形象地展示消息队列和事件循环、异步任务的运行机制:
没有异步任务时,主线程的一次执行
在主线程中引入事件循环
渲染进程的线程之间发送通知
线程模型:消息队列、事件循环和跨进程发送信息
参考
浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务
标签:异步,队列,主线,js,任务,执行 From: https://www.cnblogs.com/cpJa3/p/17300303.html