参考文档
浏览器与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
,同时会生成一个宏任务 setTimeout2
setTimeout1
在 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
的:
DOMWindowEventQueue
window.storage
,window.hashchange
,document.selectionchange
ScriptedAnimationController
Frame
调用的BeginMainFrame
函数触发,同时还管理requestAnimationFrame
请求Animation.finish
,Animation.cancel
,CSSAnimation.animationstart
,CSSAnimation.animationiteration
(CSSAnimation
)Custom dispatch
OS events
操作系统事件,timers
定时器,文档/元素生命周期事件Custom dispatch event
不通过队列,他们直接被触发(这里我猜想可能就是‘任务队列’的隐藏概念,他们不会排在普通队列中,而是单独去了另一个特殊的队列执行,这里就是‘任务队列’)Microtask
队列EndOfTaskRunner.didProcessTask()
触发,任务由TaskQueueManager
运行,每当task
完成时,微任务队列就会执行image.onerror
,image.onload
Promise 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