event loop
先了解浏览器的进程和线程
浏览器中打开新标签时,就会创建一个任务队列。每个标签都是单线程处理所有的任务。
浏览器要负责多个任务,如渲染HTML、执行JavaScript代码、处理用户交互(用 户输入、鼠标点击等)、执行和处理异步请求。
渲染进程(浏览器内核)是多线程的,也是浏览器的重点,因为页面的渲染,JS执行等都在这个进程内进行
浏览器是多进程的,浏览器每一个打开一个Tab页面都代表着创建一个独立的进程(至少需要四个,若页面有插件运行,则五个)。

渲染进程
GUI渲染线程 负责渲染浏览器界面,包括解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。 注意 : GUI渲染线程与JS引擎线程是互斥的。
JS引擎线程 也称为JS内核,负责解析处理Javascript脚本,运行代码。(例如V8引擎)。 JS引擎一直等待并处理任务队列中任务。一个Tab页中无论什么时候都只有一个JS线程在运行JS程序
定时触发器线程 setInterval和setTimeout所在线程。通过此线程计时完毕后,添加到事件队列中,等待JS引擎空闲后执行
事件触发线程 当一个事件被触发时该线程会把事件添加到事件队列,等待JS引擎的处理 这些事件可来自JS引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
异步http请求线程 在XMLHttpRequest连接后是通过浏览器新开一个线程请求。 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JS引擎执行
在浏览器中,通常有多个线程运行不同的任务,其中包括:
主线程(Main Thread):这是与用户界面(UI)交互的线程。主线程负责处理用户输入、呈现网页内容和响应事件等。这也是执行 JavaScript 代码的线程。
UI 线程(User Interface Thread):UI 线程实际上是主线程的一部分,负责绘制用户界面和处理用户交互事件。
网络线程(Network Thread):负责处理网络请求和响应,以便从服务器获取资源。
定时器线程(Timer Thread):管理计时器,执行定时任务,如setTimeout和setInterval。
事件线程(Event Thread):也称为消息队列线程,用于管理事件队列,将事件传递到主线程的事件循环中。
Event loop
Event loop是一种机制,用于协调和处理不同线程之间的任务和事件
在前端开发中,JavaScript 代码通常在主线程中执行。当执行异步操作(例如网络请求、定时器、事件处理器等)时,结果将被发送到事件队列中。然后,"Event loop" 负责按照一定的顺序将这些结果处理,并在主线程上执行相应的回调函数。
这意味着 JavaScript 中的异步操作不会阻塞主线程,使得用户界面保持响应性。"Event loop" 确保了这种非阻塞性,通过将异步任务按照它们进入事件队列的顺序依次执行。
所以,总结起来,"Event loop" 是浏览器线程中的一个机制,用于协调和处理异步任务和事件,以确保前端应用的流畅性和响应性。
宏任务可以被看作是一个个独立的任务,而微任务则是宏任务中的一个子任务,它们的执行顺序是不同的。
在事件循环中,当执行一个宏任务时,假如有微任务会被推入微任务队列中,等待当前宏任务执行完毕后被执行。因为微任务的执行时机比宏任务早,所以在下一个宏任务开始执行之前,JavaScript 引擎会先处理所有的微任务,然后才会执行下一个宏任务。这样可以保证微任务的执行顺序比宏任务早。
定时器包括 setTimeout 和 setInterval 等都是属于宏任务的一种。这是因为它们的执行时机是由浏览器的事件循环控制的,而不是由 JavaScript 引擎控制的。
执行过程
首先执行同步代码,这属于宏任务,执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
先执行"最前面"的宏任务, 然后执行当前 loop 下所有的微任务, 所有微任务完毕之后, 进入下一次 loop, 执行接下来的宏任务, 重复上述过程。 所以也不能说先宏后微,执行完任务队列头的宏任务后就开始执行微任务队列中的微任务,直到微任务队列为空。
当执行完所有
同步代码后,执行栈为空,立即执行当前微任务队列中的所有微任务(依次执行) 宏任务结束后,会执行渲染,然后执行下一个宏任务,而微任务可以理解成在当前宏任务执行后立即执行的任务。 也就是说,当宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完。
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:black'
});
console.log(3);
// 1 3 2
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 然后开始下一轮 Event Loop,开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。
JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。
宏任务-->渲染-->宏任务-->渲染-->渲染... 主代码块,setTimeout,setInterval等,都属于宏任务
第一个例子: 我们可以将这段代码放到浏览器的控制台执行以下,看一下效果: 我们会看到的结果是,页面背景会在瞬间变成灰色,以上代码属于同一次宏任务,所以全部执行完才触发页面渲染,渲染时GUI线程会将所有UI改动优化合并,所以视觉效果上,只会看到页面变成灰色。
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';
第二个例子: 我会看到,页面先显示成red背景,然后瞬间变成了黑色背景,这是因为以上代码属于两次宏任务,第一次宏任务执行的代码是将背景变成red,然后触发渲染,将页面变成red,再触发第二次宏任务将背景变成黑色。
貌似看不到red,太快?
document.body.style = 'background:red';
setTimeout(function() {
document.body.style = 'background:black'
}, 0)
// }, 10000)
宏任务有哪些?
JavaScript中的宏任务是由浏览器或Node.js环境提供的任务队列,它们会在主线程空闲时执行。以下是常见的JS宏任务类型:
setTimeout 和 setInterval:在指定的时间间隔或延迟之后执行指定的函数。
特点:setTimeout定义的操作在函数调用栈清空之后才会执行
网络请求:执行网络请求(如HTTP请求),通常通过XMLHttpRequest或fetch等方式异步获取数据的操作。
页面渲染:在页面加载、重绘或者样式计算时执行的任务。解析和渲染:解析HTML、CSS以及构建和渲染DOM树和页面布局等任务通常也作为宏任务执行。
UI 交互事件:如点击、鼠标移动、滚动、输入等,触发了事件处理,这些事件通常作为宏任务执行。。
原生事件:例如window.resize、window.scroll等。
requestAnimationFrame:用于执行动画的requestAnimationFrame方法会触发回调函数,这个回调函数作为宏任务执行,通常在每个屏幕刷新之前执行。
postMessage:通过MessageChannel或window.postMessage方法发送的异步消息。
文件I/O(Node.js环境):在Node.js环境中,文件I/O操作通常是宏任务,涉及读取或写入文件系统。
Web Workers:Web Workers是运行在独立线程中的JavaScript脚本,它们可以执行一些耗时的计算任务,这些任务通常作为宏任务。
微任务有哪些?
微任务是 JavaScript 引擎内部的任务,会在当前宏任务执行完毕后立即执行。常见的微任务有以下几种:
微任务会在当前宏任务执行完毕后,优先于下一个宏任务执行,因此可以用来在当前任务完成后立即执行一些需要尽早完成的任务,例如执行一些 DOM 更新操作,避免用户看到页面更新的延迟感。需要注意的是,由于微任务在执行顺序上的优先级比较高,如果不小心出现过多的微任务,可能会导致宏任务长时间得不到执行,从而导致页面卡顿的现象。
Promise:new Promise().then 的回调,promise构造函数是同步执行:new Promise中传入的执行器函数是同步函数,在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。 首先Promise构造函数会立即执行,而Promise.then()内部的代码在当次事件循环的结尾立即执行(微任务)。
async/await:使用async函数和await操作符时,await之后的代码通常会被放入微任务队列中,以在Promise状态发生变化时执行。
MutationObserver 回调函数:监测 DOM 树变化的回调函数。
Object.observe():监听对象变化的回调函数。
process.nextTick():在 Node.js 中的异步任务队列。
queueMicrotask():ES6 新增的微任务队列 API。
Vue nextTick 参考:api-nextTick
宏任物和微任务那个先执行?
在JavaScript中,微任务(microtasks)会在宏任务(macrotasks)之前执行。宏任务通常包括浏览器事件,如用户交互事件、定时器事件和网络请求等。微任务包括Promise的回调、MutationObserver的回调等。
当JavaScript引擎执行完一个宏任务后,会首先处理所有当前微任务队列中的微任务,然后再继续执行下一个宏任务。这确保了微任务的优先级高于宏任务,从而可以更快地响应一些重要操作,例如Promise的处理结果。
总结: 微任务(microtasks)和宏任务(macrotasks)的执行顺序都是根据它们被添加到队列的顺序来执行的,它们都遵循FIFO(先进先出)的顺序。这确保了微任务和宏任务都按照它们进入队列的顺序依次执行。
宏任务和微任务之间的关键区别在于它们在事件循环中的优先级。微任务具有较高的优先级,因此在执行宏任务队列之前,事件循环会首先清空微任务队列中的所有微任务。这可以确保微任务能够尽快执行,而不会阻塞主线程的执行。
nodejs的事件循环和浏览器的事件循环区别
Node.js 和浏览器都使用事件循环(Event Loop)来处理异步操作,但它们在实现上有一些区别。以下是 Node.js 的事件循环和浏览器的事件循环之间的主要区别:
运行环境:
- Node.js 的事件循环运行在服务器端 JavaScript 环境中,用于处理服务器端应用程序的异步操作,如文件系统操作、网络请求、数据库查询等。
- 浏览器的事件循环运行在客户端浏览器中,用于处理前端 Web 应用程序的异步操作,如用户界面事件、XHR 请求、DOM 操作等。
事件源:
- Node.js 的事件循环通常涉及处理 I/O 操作,例如文件读取、网络通信、数据库查询等,以及自定义事件。
- 浏览器的事件循环主要涉及处理用户交互和浏览器 API,例如点击事件、HTTP 请求、DOM 操作等。
API 差异:
- Node.js 提供了一些特定于服务器端的 API,如文件系统模块、
http模块、net模块等,用于处理服务器端操作。 - 浏览器提供了一些特定于客户端的 API,如 DOM 操作、
XMLHttpRequest、fetch等,用于处理前端 Web 应用程序的操作。
- Node.js 提供了一些特定于服务器端的 API,如文件系统模块、
事件循环执行顺序:
- Node.js 的事件循环通常执行完一个阶段中的所有回调,然后才继续下一个阶段。它遵循"先进先出"(FIFO)的原则。
- 浏览器的事件循环是基于浏览器事件触发的。浏览器的事件循环按照事件触发的顺序来执行回调,而不是在一个阶段内执行所有回调。
宿主环境:
- Node.js 是构建在 V8 JavaScript 引擎之上,它运行在服务器端操作系统上。
- 浏览器是一个宿主环境,支持 JavaScript 运行在客户端浏览器中。
尽管有这些区别,Node.js 的事件循环和浏览器的事件循环都遵循事件驱动的异步编程模型,通过事件处理程序来处理异步操作,以提高性能和响应性。无论是在服务器端还是客户端,了解事件循环如何工作对于有效地处理异步操作都是至关重要的。
Node.js 中的宏任务
Node.js 中的宏任务通常包括一些可能需要较长时间才能完成的任务,这些任务通常是由特定的 Node.js 模块或全局函数触发的。以下是一些常见的 Node.js 中的宏任务:
I/O 操作:
- 文件系统操作(如读取文件或写入文件)。
- 网络请求(例如使用
http、https模块发起的 HTTP 请求)。 - 数据库查询(例如使用
mysql、mongodb模块进行的查询)。
定时器任务:
- 使用
setTimeout和setInterval设置的定时器任务,它们会在指定的时间后触发。 - 例如,
setTimeout和setInterval回调函数属于宏任务。
- 使用
自定义事件:
- Node.js 允许你创建自定义事件和触发器,这些自定义事件可能触发宏任务。
- 你可以使用
EventEmitter类或其他事件处理模块来实现自定义事件。
网络通信:
- Node.js 中的网络服务器和客户端通信也可以触发宏任务,特别是当涉及到网络套接字和数据流时。
操作系统操作:
- 与操作系统交互的一些操作,如进程操作、子进程的创建和管理,也可以触发宏任务。
一些全局函数:
- 一些全局函数,如
setImmediate和fs.write,也可以触发宏任务。
- 一些全局函数,如
JS到底是怎么运行的呢?
参考:Parser解析
初始化-构造事件循环与消息队列
JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它。也就是等待宿主环境分配宏观任务,反复等待 - 执行即为事件循环。

概念1:宿主 JS运行的环境:浏览器/Node。
概念2:执行栈,是一个存储函数调用的栈结构,遵循先进后出的原则。
function foo() {
throw new Error('error')
console.log("test")
}
function bar() {
foo()
}
bar()
以上代码会报错:
VM100:2 Uncaught Error: error
at foo (<anonymous>:2:9)
at bar (<anonymous>:5:3)
at <anonymous>:7:1
当开始执行 JS 代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,在图中我们也可以发现,foo 函数后执行,当执行完毕后就从栈中弹出了。
JS是单线程的,那么单线程的JS是怎么完成非阻塞的完成异任务的呢?-->事件循环
- Node环境中,只有JS 线程。
- 在浏览器环境中,有JS 引擎线程和渲染线程,且两个线程互斥。
js是单线程语言,浏览器只分配给js一个主线程,用来执行任务(函数),但一次只能执行一个任务,这些任务形成一个任务队列排队等候执行,但前端的某些任务是非常耗时的,比如网络请求,定时器和事件监听,如果让他们和别的任务一样,都老老实实的排队等待执行的话,执行效率会非常的低,甚至导致页面的假死。
所以,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程,这些任务是异步的。
那么问题来了,这些异步任务完成后,主线程怎么知道呢?
浏览器提供一些异步的WebAPI例如DOM操作,setTimeout,XHR等,JS通过事件循环机制(event loop)调用这些API的回调。答案就是回调函数,整个程序是事件驱动的,每个事件都会绑定相应的回调函数,举个例子,有段代码设置了一个定时器:
setTimeout(function(){
console.log(time is out);
},1000);
执行这段代码的时候,浏览器异步执行计时操作,当1000ms到了后,会触发定时事件,这个时候,就会把回调函数放到任务队列里。整个程序就是通过这样的一个个事件驱动起来的。 所以说,js是一直是单线程的,实现异步的是浏览器。
event loop题目
经典案例
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i); //输出6 6 6 6 6 6
}, i*1000 );
}
因为:根据setTimeout定义的操作在函数调用栈清空之后才会执行的特点,for循环里定义了5个setTimeout操作。而当这些操作开始执行时,for循环的i值,已经先一步变成了6。因此输出结果总为6。
解决:
而我们知道在函数中闭包判定的准则,即执行时是否在内部定义的函数中访问了上层作用域的变量。因此我们需要包裹一层自执行函数为闭包的形成提供条件。 因此,我们只需要2个操作就可以完成题目需求,一是使用自执行函数提供闭包条件,二是传入i值并保存在闭包中。
//而我们想要让输出结果依次执行,我们就必须借助闭包的特性,每次循环时,将i值保存在一个闭包中,当setTimeout中定义的操作执行时,则访问对应闭包保存的i值即可。
for (var i=1; i<=5; i++) {
(function(i) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
})(i)
}
解析
这道题涉及了异步、作用域、闭包
setTimeout是异步执行,10ms后往任务队列里面添加一个任务,只有主线上的全部执行完,才会执行任务队列里的任务,当主线执行完成后,i是4,所以此时再去执行任务队列里的任务时,i全部是4了。对于打印4次是:
每一次for循环的时候,setTimeout都执行一次,但是里面的函数没有被执行,而是被放到了任务队列里面,等待执行,for循环了4次,就放了4次,当主线程执行完成后,才进入任务队列里面执行。
(注意:for循环从开始到结束的过程,需要维持几微秒或几毫秒。)
当我把var 变成let 时
for(let i=0;i<=3;i++){ setTimeout(function() { console.log(i) }, 10);}
打印出的是:0,1,2,3
当解决变量作用域,
因为for循环头部的let不仅将i绑定到for循环快中,事实上它将其重新绑定到循环体的每一次迭代中,确保上一次迭代结束的值重新被赋值。setTimeout里面的function()属于一个新的域,通过 var 定义的变量是无法传入到这个函数执行域中的,通过使用 let 来声明块变量,这时候变量就能作用于这个块,所以 function就能使用 i 这个变量了;这个匿名函数的参数作用域 和 for参数的作用域 不一样,是利用了这一点来完成的。这个匿名函数的作用域有点类似类的属性,是可以被内层方法使用的。
简单的参考例子
setTimeout(function() {
console.log('timeout1');
})
new Promise(function(resolve) {
console.log('promise1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function() {
console.log('then1');
})
console.log('global1');
/*
promise1
promise2
global1
then1
undefined
timeout1
* */