由一道面试题来探讨下浏览器的事件循环

314
setTimeout(() => {
  console.log('1111')
}, 3000);
console.log('2222')
new Promise((resolve,reject)=>{
  console.log('3333')
  resolve()
}).then(()=>{
  console.log('444')
})
console.log('end')

大家面试一定遇到过这道面试题,之前看到过很多博主分享这个问题的结果,但是看完之后还是不是很理解其中的原理,有时候换了一种写法可能就不知道输出的结果了,今天跟大家分享下以后遇到这种问题,如何以不变应万变;

首先,在这里之前我们先要了解js的知识:

js 常见的宏任务:整体script代码,setTimeoutsetInterval
js 常见的微任务:Promise,process.nextTicket

js的执行顺序:

如上代码在执行的过程中:

1.先执行非异步的代码,然后将异步代码放入到异步队列中。

2.执行完所有的非异步代码后,去查询异步队列。

3.因为所有的宏任务必须保证在所有的微任务执行完毕后才可以执行,所以先执行所有的微任务,微任务执行完成后才可以执行宏仁务:

接下来,我们一起从原理上来探讨探讨:

前言

我们知道javascript被设计成为单线程非阻塞的语言,javascript这样设计主要是为了浏览器的交互。

单线程意味者,在执行代码的时候有且仅有一个主线程来处理事件。

非阻塞表示当代码在执行的过程中遇到异步操作,主线程会将这个任务挂起,然后在异步任务返回的时候在处理对应的回调。

思考一下,如果javascript被设计成多线程的语言,如果这个时候有一个线程是修改dom,有一线程是删除dom,那么就会导混乱,所以javascipt被设计成单线程非阻塞的脚本,永远保证了程序执行的一致性;

前面说到javascipt是'非阻塞'的语言,那么接下来我们看看浏览器的底层是如果实现的?

浏览器的实践

我们知道javascript的数据主要是存储在栈(stack)和堆(heap)中,栈中主要用来存储基本数据类型以及对象的指针,堆中主要用来存储数组,对象的值,这里我想说的是浏览器的执行栈,完全不同于上面所说的栈;

我们知道js在执行一段代码的时候,js会生成一个与这个方法对应的执行栈(context),也就是执行上下文,在这个执行环境中,存在着这个方法的私有变量,作用域,这个作用域中定义的变量以及这个作用域的this对象。

当一个脚本第一次执行的时候,js会解析这段代码,并将其中的代码按照顺序加入执行栈中,然后从头开始执行,如果当前执行的是一个方法的话,那么js会向执行栈中添加这个方法的执行环境,然后进行这个执行环境继续执行其中的代码,当这个执行环境的代码执行完毕并返回结果后,js会推出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境,重复操作,依次类推,直到执行栈中的代码全部执行完毕。

下面用一个简单的图展示:

图片.png 图片.png

图片.png

图片.png

上面图片向我们展示了数据被添加进执行栈以及从执行栈中销毁的过程,这个就是同步代码在执行栈环境中的执行过程,接下来我们探讨下异步,而异步中不得不提的就是异步操作中的'非阻塞'机制-事件队列(Task-Queue)

js引擎在执行遇到一个异步事件的时候会先将他挂起(pending),然后继续执行执行栈的其他任务,等到异步任事件返回后,js会将他添加到与当前执行栈不同的事件队列中,等到当前执行栈中的任务执行完成,主线程处于闲置状态,在将事件队列的第一个事件的回调添加到执行栈中,执行同步代码,一次类推,直到事件执行完成;

宏仁务与微任务

上面的事件队列属于比较笼统,实际上的任务队列分为宏仁务队列和微任务队列;

宏仁务

setTimeout()
setInterval()

微任务

new Promise()
new MutaionObserver()

在上面我们讨论过异步事件执行后的结果会被存放在事件队列中,实际上根据这个任务的类型,他们会分别被存放在微任务和宏任务队列中,当前的执行栈为空的时候,主线程会去微任务队列中查看,是否有事件存在,如果不存在,在去宏仁务队列中查看,取出队列中的第一个事件加入到当前的执行栈中;如果微任务中存在事件,则会依次执行微任务的时间,直到微任务队列为空没在去宏仁务中取出队列中的第一个事件的回调加入到当前的执行栈中, 如此反复,直到结束。

所以上述面试题的结果如上;