JS事件循环机制是目前我们在面试中会频繁被问到的一个问题。很多人可能大概知道JS事件循环机制是个什么东西,但是对具体的概念又比较模糊,无法在面试官面前清晰地表述出来。
所以今天我就来带大家深入理解一下JS事件循环机制到底是个什么东西。
一、JS异步实现
JS是通过事件循环机制实现单线程异步的。
首先我们都知道JS是一门单线程的语言。在设计初期,由于JS是运行在浏览器端的脚本语言,目的就是为了实现与页面的动态交互,其核心就是DOM操作,这就决定了他必须使用单线程去处理脚本信息,从而避免对同一DOM元素同时进行操作时产生冲突问题。
若是遇到耗时操作,页面便会产生堵塞。例如请求接口返回数据慢,图片未加载完成等等。这样显然是不合理也不实用的,因此异步模式应运而生。
异步任务:
异步任务指的是,不进入主线程、而进入"任务队列"(taskqueue)的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。
异步模式的方式:
回调函数callback;
事件驱动Event-Driven;
观察者模式Observerpattern(又称发布订阅模式publish-subscribepattern);
Promise,async/await;
宏任务(定时器,ajax,DOM事件监听);
setImmediate(立即执行,Node.js执行环境);
mutationobserver(H5,监视DOM元素变化)。
特点:
不会等待这个任务的结束才开始下一个任务;
对于耗时操作开启过后就立即往后执行下一个任务;
耗时任务的后续逻辑一般通过回调函数的方式定义,耗时任务完成过后就会自动执行传入的回调。
二、执行栈与任务队列
执行栈:
当执行某个函数、用户点击一次鼠标、Ajax请求完成、一个图片加载完成等事件发生时,只要指定了回调函数,这些事件发生时就会进入执行栈队列中,等待主线程读取,并遵循先进先出原则。
主线程:
要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。
主线程会不停的从执行栈中读取事件,并执行完所有栈中的同步代码。
当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(TaskQueue)。
当主线程将执行栈中所有的代码执行完之后,主线程将会去查看任务队列是否有任务。如果有,那么主线程会依次执行那些任务队列中的回调函数。
异步任务:
异步任务分为宏任务(macrotask)与微任务(microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待EventLoop将它们依次压入执行栈中执行。
宏任务(macrotask):
script(整体代码)
setTimeout
setInterval
UI渲染
I/O
postMessage
MessageChannel
setImmediate(Node.js环境)
微任务(microtask):
Promise
MutaionObserver
process.nextTick(Node.js环境)
宏任务和微任务的区别:
微队列是唯一的,在整个事件循环中,仅存在一个,并且同一轮事件循环中的微任务会按顺序依次执行。
而宏任务存在一定的优先级(用户I/O部分优先级更高)。且同一轮事件循环中,只执行一个。
三、EventLoop(事件循环)
当JS代码执行时,所有任务(同步/异步)都在主线程上执行,形成一个执行栈;
执行栈之外有用于存储待执行异步回调的任务队列(taskqueue)===宏队列与微队列;
浏览器中有在其它分线程执行相关管理模块:定时器管理模块,Ajax请求管理模块,DOM事件管理模块。若碰到这些任务源,就会将其回调函数加入到宏队列中;
若碰到微任务源,例如Promise,则会将其回调函数加入到微队列中;
直至script宏任务执行结束后,就会执行微队列中的任务;
当微队列中的所有微任务执行结束,就会检查宏队列中有没有可执行的宏任务。如果有,则执行该宏任务,之后检查微队列并执行微任务,依次循环;反之,要是宏队列中没有待执行任务,则循环结束。这就是我们常说的事件循环机制。
EventLoop(事件循环)中,每一次循环称为tick,每一次tick的任务如下:
执行栈选择最先进入队列的宏任务(通常是script整体代码),如果有则执行;
检查是否存在微任务,如果存在则不停的执行,直至清空微任务队列;
更新render(每一次事件循环,浏览器都可能会去更新渲染);
重复以上步骤。
四、面试题实践
我们来分析一个简单的例子:
script
console.log("scriptstart");
Promise.resolve().then(()={
console.log("Promise1");
setTimeout(()={
console.log("setTimeout2");
},0);
});
Promise.resolve().then(()={
console.log("Promise2");
});
setTimeout(()={
console.log("setTimeout1");
Promise.resolve().then(()={
console.log("Promise3");
});
},0);
console.log("scriptend");
/script
题目解析:
主线程执行,输出‘scriptstart’,Promise直接resolve返回,then的回调属于异步微任务,放入微队列中等待执行,setTimeout1属于异步宏任务,放入宏队列中,然后执行同步任务,输出‘scriptend’;
当script宏任务执行结束,将事件队列中的微任务回调放入主线程中执行,Promise1执行打印‘Promise1’,然后将setTimeout2宏任务加入到宏队列中,之后执行第二个微任务Promise2,输出‘Promise2’;
微队列清空,setTimeout1执行,输出‘setTimeout1’,又遇到Promise3放入微任务队列;
setTimeout1执行结束,执行微队列中的Promise3,并输出‘Promise3’;
此时微队列再次清空,继续执行宏任务setTimeout2,输出‘setTimeout2’;
事件队列彻底清空,循环结束。
由此得出,浏览器最终的输出顺序应为:‘scriptstart’=‘scriptend’=‘Promise1’=‘Promise2’=‘setTimeout1’=‘Promise3’=‘setTimeout2’。
总结
综合上文,我们可以得出JS异步执行顺序也就是事件循环的机制为:
第一步:先执行script宏任务;
第二步:再依次取出微队列中的所有微任务执行==UI线程更新界面;
第三步:再取出宏队列中第一个宏任务执行;
第四步:再依次取出微队列中的所有微任务执行==UI线程更新界面;
后面重复三,四步。