前端开发JS事件循环机制知多少

中科白癜风抗复发治疗 https://m.39.net/baidianfeng/a_8308723.html

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线程更新界面;

后面重复三,四步。




转载请注明:http://www.aierlanlan.com/rzgz/7590.html