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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,928评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,748评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,282评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,065评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,101评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,855评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,521评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,414评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,931评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,053评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,191评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,873评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,529评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,074评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,188评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,491评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,173评论 2 357

推荐阅读更多精彩内容

  • 文章首次发表在 个人博客 前言 最近面试了很多家公司,这道题几乎是必被问到的一道题。之前总觉得自己了解得差不多,但...
    IOneStar阅读 4,190评论 0 10
  • 答题大纲 先说基本知识点,宏任务、微任务有哪些 说事件循环机制过程,边说边画图出来 说async/await执行顺...
    小虫000阅读 967评论 0 1
  • 事件循环Event Loop JavaScript语言的一大特点就是单线程,作为脚本语言,避免复杂性。因为如果是多...
    ERICOOLU阅读 385评论 0 1
  • 一道面试题 说出下面代码的运行结果,并说明原因: 先贴一下在浏览器里的运行的结果(如果跟你的思路一模一样的话,大佬...
    hui树阅读 290评论 0 0
  • 大家好,我是前端dog君,一名95后前端小兵。2019年毕业于北京化工大学,天津人,不知道有校友和老乡嘛?对前端的...
    前端dog君阅读 296评论 1 2