JavaScript面试考点之事件循环

1、Javascript中的事件循环机制

首先,因为JavaScript是一门单线程的语言。意味着着同一时间内只能做一件事,那么就会存在阻塞现象(比如一个线程删除了这个DOM节点,一个线程需要操作这个DMO节点就出现冲突),而实现单线程非阻塞的方法就是事件循环。

在JavaScript中,所有的任务都可以分为

1)同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行。

2)异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等。异步任务还可以细分为微任务与宏任务。不同的任务源会被分配到不同的Task队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在ES6规范中,microtask称为jobs,macrotask称为task。

常见的宏任务:整体代码,setTimeout,setInterval,setImmediate,I/O操作,postMessage、MessageChannel,UI rendering/UI事件

常见的微任务:new Promise().then,MutaionObserver(前端的回溯),Object.observe(已废弃;Proxy 对象替代),process.nextTick(Node.js)。

为什么进入微任务的概念?只有宏任务可以吗?由于回调函数的执行顺序是遵循先进先出的原则。如果存在高优先级的的任务,回调函数无法立即执行,所以引入了微任务的概念。宏任务执行完一遍后,先去微任务队列把微任务执行完后再进行下一轮。

JS中的事件循环机制

1)执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中。

2)当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完。循环往复,直到两个 queue 中的任务都取完。

Event loop 顺序:

a、执行同步代码,这属于宏任务

b、执行栈为空,查询是否有微任务需要执行

c、执行所有微任务

d、必要的话渲染 UI

e、然后开始下一轮Event loop,执行宏任务中的异步代码

通过上述的Event loop顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作DOM的话,为了更快的响应界面响应,我们可以把操作DOM放入微任务中。

例题1:

解析:遇到 console.log(1) ,直接打印 1; 遇到定时器,属于新的宏任务,留着后面执行;遇到 new Promise,这个是直接执行的,打印 'new Promise'; .then 属于微任务,放入微任务队列,后面再执行;遇到 console.log(3) 直接打印 3; 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then';当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2;

第一轮:主线程开始执行,遇到setTimeout,将setTimeout的回调函数丢到宏任务队列中,在往下执行new Promise立即执行,输出2,then的回调函数丢到微任务队列中,再继续执行,遇到process.nextTick,同样将回调函数扔到为任务队列,再继续执行,输出5,当所有同步任务执行完成后看有没有可以执行的微任务,发现有then函数和nextTick两个微任务,先执行哪个呢?process.nextTick指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick输出4然后执行then函数输出3,第一轮执行结束。

第二轮:从宏任务队列开始,发现setTimeout回调,输出1执行完毕,因此结果是25431

面试回答:

首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行

在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务

当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行

任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行

当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。

2、Node中 的事件循环机制

Node中的微任务存在2种:process.nextTick() 注册的回调 (nextTick task queue);promise.then() 注册的回调 (promise task queue)

Node 在执行微任务时, 会优先执行 nextTick task queue 中的任务,执行完之后会接着执行 promise task queue 中的任务。所以如果 process.nextTick 的回调与 promise.then 的回调都处于主线程或事件循环中的同一阶段,process.nextTick 的回调要优先于  promise.then 的回调执行。

Node中的宏任务存在4种:setTimeout、setInterval、setImmediate和I/O。

宏任务在微任务执行之后执行,因此在同一个事件循环周期内,如果既存在微任务队列又存在宏任务队列,那么优先将微任务队列清空,再执行宏任务队列。

Node中事件循环分为六个阶段:

由于Pending callbacks、Idle/Prepare 和 Close callbacks 阶段是 Node 内部使用的三个阶段,所以这里主要分析与开发者代码执行更为直接关联的Timers、Poll 和 Check 三个阶段。

1)Timers(计时器阶段):初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。

2)Pending callbacks:执行推迟到下一个循环迭代的I / O回调(系统调用相关的回调)。

3)Idle/Prepare:仅供系统内部使用。

4)Poll(轮询阶段)

a、当回调队列不为空时:

会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后,就执行相应微任务。执行完所有的回掉后,变为下面的情况。

b、当回调队列为空时(没有回调或所有回调执行完毕):

但如果存在有计时器(setTimeout、setInterval和setImmediate)没有执行,会结束轮询阶段,进入 Check 阶段。否则会阻塞并等待任何正在执行的I/O操作完成,并马上执行相应的回调,直到所有回调执行完毕。

5)Check(查询阶段):会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。

6)Close callbacks:执行一些关闭回调,比如socket.on('close', ...)等。

注意:宏任务和微任务在node中的执行顺序。

在Node V10及以前,执行完一个阶段的所有任务,再执行process.nextTick 的回调,再执行微任务队列的内容。如promise.then 的回调执行。

Node V10及以后和浏览器一致。

解析:第一个事件循环主线程发起,因此先执行同步代码,所以先输出 start,然后输出 end;

再从上往下分析,遇到微任务,插入微任务队列,遇到宏任务,插入宏任务队列,分析完成后,微任务队列包含:Promise.resolve 和 process.nextTick,宏任务队列包含:fs.readFile 和 setTimeout;

先执行微任务队列,但是根据优先级,先执行process.nextTick 再执行 Promise.resolve,所以先输出nextTick callback再输出Promise callback;

再执行宏任务队列,根据宏任务插入先后顺序执行 setTimeout 再执行 fs.readFile,这里需要注意,先执行setTimeout由于其回调时间较短,因此回调也先执行,并非是setTimeout先执行所以才先执行回调函数,但是它执行需要时间肯定大于1ms,所以虽然fs.readFile先于setTimeout执行,但是setTimeout执行更快,所以先输出setTimeout,最后输出read file success。

输出结果:startendnextTick callbackPromise callbacksetTimeoutread file success

解析:在上面代码中,有 2 个宏任务和 1 个微任务,宏任务是setTimeout 和 fs.readFile,微任务是Promise.resolve。

整个过程优先执行主线程的第一个事件循环过程,所以先执行同步逻辑,先输出 2。

接下来执行微任务,输出poll callback。

再执行宏任务中的fs.readFile 和 setTimeout,由于fs.readFile优先级高,先执行fs.readFile。但是处理时间长于1ms,因此会先执行setTimeout的回调函数,输出1。这个阶段在执行过程中又会产生新的宏任务fs.readFile,因此又将该fs.readFile 插入宏任务队列

最后由于只剩下宏任务了fs.readFile,因此执行该宏任务,并等待处理完成后的回调,输出read file sync success。

输出结果:2   poll callback1    read file success    read file sync success

3、async与await

async是异步的意思,await则可以理解为async wait。所以可以理解async就是用来声明一个异步方法,而await是用来等待异步方法执行。

1)async 函数返回一个promise对象

2)await 正常情况下,await命令后面是一个Promise对象,返回该对象的结果(理解为new Promise())。如果不是Promise对象,就直接返回对应的值。不管await后面跟着的是什么,await都会阻塞后面的代码(理解为new Promise().then()的代码)。

示例1:

await会阻塞下面的代码(即加入微任务队列),先执行async外面的同步代码,同步代码执行完,再回到async函数中,再执行之前阻塞的代码。

输出结果:1 fn2 3 2

示例2:

解析:首先遇到console.log('script start'),直接打印结果,输出script start;遇到定时器将其放入宏任务队列中;遇到async1(),执行它,遇到console.log('async1 start'),输出async1 start;遇到await async2(), 执行async2() ,然后阻塞下面代码(即加入微任务列表);遇到console.log('async2'),输出script async2;跳到new promise(),直接打印promise1;有resolve(),把then()后面放入微任务队列;打印最后一行console.log('script end')。上一轮宏任务执行结束,依次执行微任务队列的任务:await阻塞的的代码console.log('async1 end'); promise().then()的代码console.log('promise2');完成再执行宏任务队列setTimeout中的console.log('settimeout')。

输出结果为:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout。

示例3:

解析:首先打印start;遇到setTimeout放入宏任务中;遇到new Promise(),执行打印children4;遇到setTimeout,放入宏任务中;由于此promise还没有返回结果所以then不会执行且不会放入微队列中;宏任务一轮结束,此时微任务队列中没有任务可执行;执行宏任务队列,打印children2;遇到Promise()且直接返回成功,则将其then放入微队列中。宏任务执行结束,执行队列微任务,打印children3;执行下一个宏任务setTimeout,打印children5;Promise()且直接返回成功结果,将then()放入微队列。此时宏任务执行结束,执行微任务,即刚放的then(),打印children7,遇到setTimeout放入宏任务中; 执行宏任务,执行setTimeout,打印children6;

输出结果为:start、children4、children2、children3、children5、children7、children6。

示例4:

解析:执行p,返回一个Promise对象,执行promise,定义p1,紧接着执行p1,遇到setTimeout,放入宏任务队列,返回成功的回调resolve(2),将p1的then()放入微任务队列,打印3;返回p的成功回调结果resolve(2),将将p的then()放入微任务队列中;继续执行代码,console.log("end"),打印end;一轮宏任务结束,依次执行微任务队列,执行p1的then()(此时setTimeout中的resolve(1)失效,因为promise的状态只能改变一次),打印2;执行p的then(),打印4;

输出结果为:3、end、2、4。

若把代码resolve(2)注释掉,输出结果为:3、end、4、1。

你可能感兴趣的:(JavaScript面试考点之事件循环)