前端面试-javascript事件循环机制(浏览器)

111 阅读5分钟

javascript单线程

浏览器event loop遵循HTML5标准.

JavaScript 从一开始被创造出来就使用的单线程,这主要与他的用途相关。

如果JavaScript是多线程程序,如多个线程对同一个 dom 进行修改以后,那浏览器会采取哪一个呢,这个无法确定。

JavaScript单线程并不是说程序运行真的只是依赖一条线程,他实际有多条协助线程,只有一条主线程来调度协助线程,协助线程会用来做一些耗时任务,这样做是为了防止耗时任务阻碍了网页响应用户的操作,提升网页性能等。

涉及到的线程如下:

  • JavaScript引擎线程:JavaScript同步任务、回调任务执行的场所,JavaScript程序调度中心
  • 异步http线程:页面ajax等网络请求任务处理等待响应的线程
  • 事件触发线程:存放任务队列的场所,异步任务完成以后触发的事件都会存放到这个线程中,这个线程中存在多个任务队列。
  • 定时器线程:用来给定时任务定时
  • GUI线程:将页面从文档处理成位图,处理页面渲染、重绘、回流等任务

其次: JavaScript单线程无法很好的利用现代多核CPU计算机,因此在HTML5中提出了 web worker标准,允许JavaScript创建多个线程来处理任务。但是子线程完全受主线程控制,并且子线程无法操作DOM。

循环执行流程

JavaScript中同步任务都需要在主线程执行栈中运行,只有当前面任务执行完成以后才能处理运行后面的同步任务。

主线程运行时,会产生堆和栈,执行栈中运行的时候会去调用一些API,如果调用的是异步函数API,如处理I/O(ajax请求)、定时器、DOM事件监听等,执行栈就会将这些异步任务挂到对应的线程中,然后执行栈再运行其他同步任务。这些被分配到其他线程中的任务只有当事件触发的时候,异步线程就会将带有回调函数的事件放入到事件触发线程中的事件队列里面去。

被放到事件队列里面的任务不会立即执行,需要等待主线程主动读取这些事件,然后在执行栈中执行这些任务的回调函数。

当JavaScript执行栈处于空闲的状态时,主线程就会主动去查看事件队列是否存在未处理的事件。

如果存在,主线程就会读取队列中第一个事件,并将这个事件对应的回调函数放入到执行栈中,然后执行里面的同步代码,执行完后就又去判断事件队列是否为空,如此往复。 如果不存在,主线程也会不停的去判断事件队列中是否有待处理的事件

任务分类 事件触发线程中存在多个任务队列,异步线程中触发的事件都会将事件存放到这些任务队列中。

这些任务可以分为两类,microtask(微任务)、macrotask(宏任务),在事件触发线程中微任务队列只能有一个,而宏任务队列就可以有多个,实际我们平时开发时用到的宏任务队列也只用到了一个。

对于微宏任务,主线程调用这些任务也是有一定的顺序,下面将介绍一下微任务和宏任务的调用顺序:

主线程读取一个宏任务执行,执行完毕后,执行下一步。(程序开始的时候只有 script 中的代码,因此只能运行 script 中的代码) 当执行栈处于空闲状态时,主线程会先判断微任务队列是否为空,不为空就读取微任务队列中的第一个任务,放到执行栈中执行。执行完以后,再判断微任务队列是否为空,如果有,再读取再执行,如此往复,直到微任务队列为空。然后读取宏任务队列。

测试题目

image.png

  • 执行script代码,调用setTimeout 计时器API,将定时任务交给定时器线程来做。后面不停的检测事件触发线程中的任务队列是否为空
  • 定时器没有设置时间,浏览器给出默认定时时间((HTML5标准规定setTimeout最小计时4ms,但是各个浏览器实现并没有按照规定来,谷歌是0-1这两个值不太清楚是哪一个)),定时完成,将对应的事件压入宏任务队列中
  • 主线程检测到宏任务队列不为空,读取队列中的第一个任务,将任务的对调函数放到执行栈中执行,调用Promise.resolve()函数,将then定义的回调函数压入微任务队列中,此时宏任务队列还有个任务等待执行
  • 主线程执行宏任务后,检测微任务队列是否为空,不为空依次读取微任务队列中的任务,直到微任务队列为空
  • 检测宏任务队列是否为空,不为空,继续上面第三步。。。