本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
简单聊聊JavaScript中的事件循环
zhuxiaoqiang · 180浏览 · 发布于2023-02-09 +关注

js的事件循环(event-loop)是我们前端学习中非常重要的一部分,也是面试中经常被问到的点,这篇文章我们就来给大家着重讲解一下吧

为什么js是单线程的

我们首先要考虑下js作为浏览器脚本语言,主要用途就是和用户互动和操作DOM。比如js同时有两个线程,两个线程同时操作同一个DOM,比如一个给DOM添加内容,一个移除DOM,那到底该听谁的呢?所以这就决定了它只能是单线程,否则就会出现一些奇怪的问题。

浏览器

我们每打开一个tab页就会产生一个进程

浏览器都包含哪些进程呢

浏览器进程

  • 浏览器的主进程(负责协调、主控),该进程只有一个

  • 负责浏览器界面显示,用户交互。如前进,后退等

  • 负责各个页面的管理,创建和销毁其他进程

  • 将渲染(renderer)进程得到的内存中的Bitmap(位图),绘制到用户界面上

  • 网络资源的管理,下载等

第三方插件进程

每种类型的插件对应一个进程,当使用该插件时才创建。因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响

GPU进程

该进程只有一个,用于3D绘制等

渲染进程

  • 通常所说的浏览器内核(Renderer进程,内部是多线程)

  • 每个Tab页面都有一个渲染进程,互不影响

  • 主要作用为页面渲染,脚本执行,事件处理等

网络进程

主要负责页面的网络资源加载。

如果浏览器是单进程,那么当一个tab页面崩溃了,就会影响到整个浏览器。同时如果插件崩溃了也会影响整个浏览器。浏览器进程有很多,每个进程又有很多的线程,都会占用内存。进程之间的内容相互隔离,这是为了保护操作系统中进行互不干扰的技术,每一个进程只能访问自己占有的数据,也就避免了进程A写入数据到进程B的情况。因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。

渲染进程

页面的渲染、js的执行、事件的循环、都在渲染进程中执行,所以重点看下渲染进程。渲染进程是多线程的,下面看下比较常用的几个线程

GUI线程

负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。

当修改了一些元素的颜色或者背景色,页面就会重绘(Repaint)

当修改元素的尺寸,页面就是重排也叫回流(Reflow)

当页面需要重绘和重排的时候GUI线程执行,绘制页面

重绘和重排的成本比较高,尽量避免重绘和重排

GUI线程和JS引擎线程是互斥的

  • 当JS引擎执行时,GUI线程会被挂起

  • GUI更新会被保存在一个队列中,等JS引擎空闲的时候立即被执行。

JS引擎线程

JS引擎线程就是JS内核,负责处理JavaScript脚本程序(例如V8引擎)

JS引擎线程负责解析JavaScript脚本,运行代码

JS引擎一直等待任务队列的到来,然后加以处理

  • 浏览器同时只能有一个JS引擎线程在运行JS程序,所以JS是单线程运行的

  • 一个Tab页在Renderer进程中无论什么时候都只有一个JS线程在运行JS程序

GUI线程和JS引擎是互斥的,JS引擎线程会阻塞GUI渲染线程

如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

事件触发线程

  • 属于浏览器而不是JS引擎,用来控制事件循环,并且管理着一个事件队列(task queue)

  • 当JS引擎执行事件绑定和一些异步操作如SetTimeOut时,也可能是浏览器内核的其他线程,如鼠标点击、Ajax异步请求等,会走事件触发线程将对应的事件添加到对应的线程中(比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待js引擎线程空闲时来处理。

  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理

  • JS是单线程,所以这些待处理队列中的事件都会排队等待JS引擎处理

定时触发器线程

  • setInterval与setTimeout所在线程

  • 浏览器定时计数器并不是由JS引擎计数的(因为JS引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确性)

  • 通过单独线程来计时并触发定时(计时完毕后,添加到事件触发线程的事件队列中,等待JS引擎空闲后执行)

  • 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

异步HTTP请求线程

  • 在XMLHttpRequest在连接后是通过浏览器新开的一个线程请求

  • 将检测到状态变更时,如果设置有回调函数,异步线程就会产生状态变更事件,将这个回调再放入事件队列中再由JS引擎执行

  • 简单说就是当执行到一个http异步请求时,就把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),再把回调函数添加到事件队列,等待js引擎线程来执行

下面就来谈谈我们的重头戏

事件循环

  • JS被分为同步任务和异步任务。

  • 同步任务在主线程(JS引擎线程)上执行,形成一个执行栈。

  • 除了主线程之外,事件触发线程管理这一个任务队列,只要异步任务有了结果,就会在任务队列中放入异步任务的回调。

  • 当执行栈中所有的同步任务执行完毕后,就会读取任务队列,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中,开始执行。 我们画个图来表示一下

let setTimeoutCallBack = function() {
  console.log('我是定时器回调');
};
let httpCallback = function() {
  console.log('我是http请求回调');
}
 
// 同步任务
console.log('我是同步任务1');
 
// 异步定时任务
setTimeout(setTimeoutCallBack,1000);
 
// 异步http请求任务
ajax.get('/info',httpCallback);
 
// 同步任务
console.log('我是同步任务2');

我们来看下这段代码。解析一下执行过程

  • js会从上到下依次执行,可以先理解为这段代码的执行环境就是主线程,也就是当前执行栈

  • 首先 执行console.log('我是同步任务1');

  • 然后执行到setTimeout时候,会交给定时器线程,并告诉定时器线程在1s后将setTimeoutCallBack回调交给事件触发线程,1s后事件触发线程把这个回调添加到了任务队列中等待执行

  • 接着执行ajax,会交给异步HTTP请求线程发送网络请求,请求成功后,将回调httpCallback交给事件触发线程并放入任务队列中等待执行。

  • 接着执行console.log('我是同步任务2');

  • 此时主线程执行栈执行完毕,js引擎线程已经空闲,开始询问事件触发线程的任务队列中是否有需要执行的回调,如果有则将任务队列中的回调事件加入执行栈中,开始执行,如果任务队列中没有需要执行的回调,js引擎会不断的发起询问,直到有为止。

浏览器上的所有线程是的行为都很单一。

  • 定时触发线程只管理定时器并只关心定时不关注结果,定时结束后就把回调交给事件触发线程

  • 异步HTTP请求线程只关心http请求不关心结果,请求结束后就把回调交给时间触发线程

  • 事件触发线程只将异步回调加入事件队列

  • JS引擎线程则执行执行栈中的事件,执行栈中的代码执行完毕后,就会询问事件触发线程的事件队列是否有回调需要执行,然后把事件队列中的事件添加到执行栈中执行,这样的反反复复的行为我们称为事件循环。

了解了事件循环下面看下宏任务和微任务

宏任务 微任务

宏任务

  • 渲染事件(如解析 DOM、计算布局、绘制)

  • 用户交互事件(如鼠标点击、滚动页面、放大缩小等)

  • JavaScript 脚本执行事件

  • 网络请求完成、文件读写完成事件

为了协调这些任务能够有序的在主线程上执行,页面进行引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,主线程不断的从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务

常见的宏任务:

  • 主代码块

  • setTimeOut

  • setInterval

  • setImmediate -- node

  • requestAnimationFrame -- 浏览器 JS引擎线程和GUI渲染线程是互斥的,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务结束后,在一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染

微任务

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束后之前。

异步回调有两种方式

  • 把异步回调函数封装成一个宏任务,添加到消息队列中,当循环系统执行到该任务的时候执行回调函数

  • 执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,这通常都是以微任务的形式体现的

我们知道宏任务结束后,会执行渲染,然后执行下一次宏任务,微任务可以理解为当前宏任务执行后立即执行的任务。

常见的微任务:

  • process.nextTick()--node

  • Promise.then()

  • catch

  • finally

  • Object.observe

  • MutationObserver

当执行完一个宏任务之后,会立即执行期间所产生的所有微任务,然后执行渲染

宏任务微任务的执行流程

浏览器首先会执行一个宏任务,然后执行当前执行栈所产生的微任务,然后再渲染页面,然后再执行下一个宏任务

面试题

function test() {
  console.log(1)
  setTimeout(function () {  // timer1
    console.log(2)
  }, 1000)
}
 test();
 setTimeout(function () {        // timer2
  console.log(3)
})
 new Promise(function (resolve) {
  console.log(4)
  setTimeout(function () {  // timer3
    console.log(5)
  }, 100)
  resolve()
}).then(function () {
  setTimeout(function () {  // timer4
    console.log(6)
  }, 0)
  console.log(7)
})
 console.log(8)

下面我们来分析一下整体的流程

首先应该找到同步任务先执行

  • 当test()调用的时候 console.log(1)会先执行,打印1。而setTimeout(我们记作timer1)作为宏任务加入宏任务队列

  • test下面的setTimtout(我们记作timer2)作为宏任务加入宏任务队列

  • new Promise()的executer中中也会当做同步任务执行 所以console.log(4)打印4。而setTimeout(我们记作timer3)作为宏任务加入宏任务队列

  • 接着promise.then()作为微任务加入微任务队列

  • 最后console.log(8)作为同步任务执行,打印8

我们再看异步任务

  • 我们当前的执行栈本身就是宏任务,宏任务执行完了之后应该立即执行微任务,这里的微任务只有Promise.then(),而setTimeout(我们记作timer4)作为宏任务加入宏任务队列,然后执行console.log(7)打印7

  • 微任务执行完毕之后,要执行GUI渲染,我们代码中没有

  • 执行宏任务队列,此时宏任务队列里面有 timer1、timer2、timer3、timer4

  • 按照定时时间,可以排列为:timer2、timer4、timer3、timer1依次拿出放入执行栈末尾执行

  • 执行timer2,console.log(3)作为同步任务打印3,然后检查有没有微任务和GUI渲染

  • 执行timer4,console.log(6)作为同步任务打印6,然后检查有没有微任务和GUI渲染

  • 执行timer3,console.log(5)作为同步任务打印5,然后检查有没有微任务和GUI渲染

  • 执行timer1,console.log(2)作为同步任务打印2,然后检查有没有微任务和GUI渲染 所以最终结果为:1、4、8、7、3、6、5、2


相关推荐

PHP实现部分字符隐藏

沙雕mars · 1312浏览 · 2019-04-28 09:47:56
Java中ArrayList和LinkedList区别

kenrry1992 · 896浏览 · 2019-05-08 21:14:54
Tomcat 下载及安装配置

manongba · 957浏览 · 2019-05-13 21:03:56
JAVA变量介绍

manongba · 953浏览 · 2019-05-13 21:05:52
什么是SpringBoot

iamitnan · 1076浏览 · 2019-05-14 22:20:36
加载中

0评论

评论
我爱编程,我爱工作,更爱生活
小鸟云服务器
扫码进入手机网页