参考文档
浏览器与Node的事件循环(Event Loop)有何区别?
事件循环机制EventLoop
JavaScript:event loop详解

堆表示一大块非结构化的内存区域,对象被存放在堆中.
栈在javascript中又称执行栈,调用栈,是一种后进先出的数组结构,调用栈(或执行栈call-stack),主线各所有的任务都会被放到调用栈等待主线程执行。
JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。
举个例子:
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7)); // 返回 42
当调用bar 时,创建了第一个帧 ,帧中包含了bar 的参数和局部变量。当 bar 调用 foo 时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了foo 的参数和局部变量。当 foo返回时,最上层的帧就被弹出栈(剩下 bar函数的调用帧 )。当 bar 返回的时候,栈就空了。
这里的堆栈,是数据结构的堆栈,不是内存中的堆栈(内存中的堆栈,堆存放引用类型的数据,栈存放基本类型的数据)
队列即任务队列Task Queue,是一种先进先出的一种数据结构。在队尾添加新元素,从队头移除元素。

简单来说浏览器内核是通过取得页面内容、整理信息(应用 CSS)、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
——|GUI 渲染线程
——|JavaScript 引擎线程
——|定时触发器线程
——|事件触发线程
——|异步 http 请求线程
GUI渲染线程HTML、CSS,构建DOM树,布局和绘制等。该线程与JS 引擎线程互斥,当执行JS 引擎线程时,GUI渲染会被挂起,当任务队列空闲时,JS引擎才会去执行GUI渲染。
JavaScript 脚本,执行代码。JS 引擎线程的执行。当然,该线程与GUI渲染线程互斥,当 JS 引擎线程执行 JavaScript 脚本时间过长,将导致页面渲染的阻塞。
setTimeout,setInterval。主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计时完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待JS引擎线程执行。
主要负责将准备好的事件交给 JS 引擎线程执行。
比如 setTimeout 定时器计数结束, ajax 等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待JS引擎线程的执行。
主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待 JS 引擎线程执行。
事件循环中的异步队列有两种:macro(宏任务)队列和micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个。
常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等。
常见的 micro-task比如: process.nextTick、new Promise().then(回调)、MutationObserver(html5 新特性) 等。
一个完整的 Event Loop过程,可以概括为以下阶段:

micro队列空,macro队列里有且只有一个 script 脚本(整体代码)。全局上下文(script 标签)被推入执行栈,同步代码执行。在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的 macro-task与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。
上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task出队时,任务是一个一个执行的;而 micro-task出队时,任务是一队一队执行的。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。
Web worker任务,如果有,则对其进行处理我们总结一下,每一次循环都是一个这样的过程:

当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。
接下来我们看道例子来介绍上面流程:
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
})
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0)
调用栈的执行顺序是后进先出
最后输出结果是 Promise1,setTimeout1,Promise2,setTimeout2
Promise1,同时会生成一个宏任务 setTimeout2setTimeout1 在 setTimeout2 之前,先执行宏任务 setTimeout1,输出 setTimeout1在执行宏任务 setTimeout1 时会生成微任务 Promise2 ,放入微任务队列中,接着先去清空微任务队列中的所有任务,输出Promise2
清空完微任务队列中的所有任务后,就又会去宏任务队列取一个,这回执行的是 setTimeout2
eventLoop = {
taskQueues: {
events: [], // UI events from native GUI framework
parser: [], // HTML parser
callbacks: [], // setTimeout, requestIdleTask
resources: [], // image loading
domManipulation: []
},
microtaskQueue: [
],
nextTask: function() {
// Spec says:
// "Select the oldest task on one of the event loop's task queues"
// Which gives browser implementers lots of freedom
// Queues can have different priorities, etc.
for (let q of taskQueues)
if (q.length > 0)
return q.shift();
return null;
},
executeMicrotasks: function() {
if (scriptExecuting)
return;
let microtasks = this.microtaskQueue;
this.microtaskQueue = [];
for (let t of microtasks)
t.execute();
},
needsRendering: function() {
return vSyncTime() && (needsDomRerender() || hasEventLoopEventsToDispatch());
},
render: function() {
dispatchPendingUIEvents();
resizeSteps();
scrollSteps();
mediaQuerySteps();
cssAnimationSteps();
fullscreenRenderingSteps();
animationFrameCallbackSteps();
intersectionObserverSteps();
while (resizeObserverSteps()) {
updateStyle();
updateLayout();
}
paint();
}
}
while(true) {
task = eventLoop.nextTask();
if (task) {
task.execute();
}
eventLoop.executeMicrotasks();
if (eventLoop.needsRendering())
eventLoop.render();
}
事件循环并没有多说关于什么时候dispatch event:
1,每一个queue(队列)中的事件都是按顺序执行;
2,事件可以直接dispatch,绕过task queues(任务队列)
2,微任务是在一个task执行完成后立即执行;
3,渲染部分循环是在vSync上执行,并且按以下顺序传递事件:
1??分派待处理的UI事件,
2??resize事件,
3??scroll滚动事件,
4??mediaquery监听者(css @media),5??CSSAnimation事件,
6??Observers,
7??requestAnimationFrame
下面来详细说一下
UI events有两类:
Discrete,俗称离散事件,就是那些不连续的,比如(mousedown,mouseup,touchstart,touchend)等Continuous,俗称连续事件,就是连续的,比如(mousemove,mousewheel,touchmove,wheel)等UI event task queue中,相匹配的连续事件(比如持续更新position属性,或者持续改变大小),可能会合并,不管那些合并的事件是否被dispatch了,因为有的还没有被dispatch,或者排在了队列的后面。dispatch,如果此时队列中有连续事件,就必须立即运行所有的连续事件,以防止离散事件的延迟。也就是说,触发离散事件的时候,连续事件必定已经全部dispatch完毕。不同的浏览器对事件循环的顺序是不同的,我还是习惯以谷歌为准,因为谷歌浏览器是最接近规范的
下面列举一些谷歌dispatch event是通过哪些方法来dispatch的:
DOMWindowEventQueuewindow.storage,window.hashchange,document.selectionchangeScriptedAnimationControllerFrame调用的BeginMainFrame函数触发,同时还管理requestAnimationFrame请求Animation.finish,Animation.cancel,CSSAnimation.animationstart,CSSAnimation.animationiteration(CSSAnimation)Custom dispatchOS events操作系统事件,timers定时器,文档/元素生命周期事件Custom dispatch event 不通过队列,他们直接被触发(这里我猜想可能就是‘任务队列’的隐藏概念,他们不会排在普通队列中,而是单独去了另一个特殊的队列执行,这里就是‘任务队列’)Microtask队列EndOfTaskRunner.didProcessTask()触发,任务由TaskQueueManager运行,每当task完成时,微任务队列就会执行image.onerror,image.onloadPromise callbacks,这里注意,Promise的回调才是真正的微任务,之前说的可能不严谨,执行promise的时候本身还是正常task关于Timer有几个方法:
requestIdleCallback
这个方法只有浏览器空闲的时候才会有内部的timer触发
requestAnimationFrame
由ScriptedAnimationController触发,这个方法挺重要的,主要是用来解决setTimeout和setInterval无法完成的动画效果。
Timers:setTimeout,setInterval
由运行在TaskQueue primitive上的WebTaskRunner触发的。
观察者分为两种,MutationObserver和IntersectionObserver。
MutationObserver:属于微任务,主要是负责监听DOM节点内的变化,前一篇随笔里有提到;
IntersectionObserver:属于轮询,可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段。具体用法不多介绍,不过它可以用来实现懒加载、无限滚动,监听元素是否在视窗中,或者已经出现了多少内容。
Promises
执行完成后,回调会放到微任务队列中去。
以上就是event loop的全部相关内容。
原文:https://www.cnblogs.com/iamsmiling/p/10705713.html