事件轮询(event loop)

一、进程与线程

  1. 进程:是cpu资源分配的最小单位
    (程序运行需要它自己的专属空间,可以把这块内存空间简单的理解为进程;
    每个应用至少一个进程,进程之间相互独立,即使要通信也要双方同意【比如王者荣耀用微信登录】)
  2. 线程:是cpu任务调度和执行的最小单位
    (一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程
    如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程【比如王者荣耀可以在后台下载皮肤道具资源包】

浏览器是一个多进程多线程的应用程序

为了避免相互影响,减少连环崩坏的几率,当启动浏览器后,它会自动启动多个进程(可以在浏览器的任务管理器中查看当前所有进程)

主要的进程有:

  1. 浏览器进程
    主要负责界面展示、用户交互、子进程管理等。
    浏览器进程内部会启动多个线程处理不同的任务。
  2. 网络进程
    负责加载网络资源。
    网络进程内部会启动多个线程来处理不同的网络任务。
  3. 渲染进程
    渲染进程启动后,会开启一个渲染主线程,主线程负责执行HTML、CSS、JS代码。
    默认情况下,浏览器会为每个标签页开启一个渲染进程,以保证不同的标签页互不影响。

注意:浏览器的渲染进程是多线程的,主要线程如下:

1、GUI渲染线程

  • 负责渲染浏览器界面,解析HTML、CSS,构建DOM树和RenderObject树,布置和绘制等;
  • 当界面需要重绘或者回流时,该线程就会执行;
  • GUI渲染线程与JS引擎线程是相互冲突的,当JS引擎线程执行时,GUI渲染线程会被挂起,并被保存在一个队列中,等到JS引擎线程空闲时才会立即执行。

2、JS引擎线程

  • 也称JS内核,负责解析javascript脚本程序,运行代码;(例如V8引擎)

3、事件触发线程

  • 归属于浏览器,不属于js引擎,用来控制时间循环;
  • 当js引擎执行代码块如setTimeout时(或者来自浏览器内核的其他线程,如鼠标点击、AJAX请求等),会将对应任务添加到事件线程中,当对应的事件符合触发条件被触发时,该线程会把事件放置于待处理队列的队尾等待JS的处理。

4、定时触发器线程

  • setTimeout和setInterval所在的线程
  • 浏览器定时计数器不是由js引擎控制的(因为js引擎线程会阻塞影响计时的准确性),是单独线程定时并触发定时(计时完毕后被添加到等待队列,等JS引擎空闲时执行)。

5、异步http请求线程

  • 在XMLHttpRequest在连接后通过浏览器新开的一个线程请求;
  • 当检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,再由JavaScript引擎执行。

多线程:是指程序中包含多个执行流,允许单个进程创建多个并行执行的线程来完成各自的任务。

优点:提高cpu的利用率。(在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。)

缺点:

  1. 线程也是程序,需要占据内存,线程越多占用的内存越多;
  2. 多线程需要协调和管理,所以需要cpu时间跟踪线程;
  3. 线程之间对共享资源的访问会互相影响;
  4. 线程太多会导致控制太复杂,最终造成多种bug。

二、浏览器渲染流程

  1. 浏览器内核拿到内容后,渲染流程大致如下:
  2. 解析HTML,构建Dom树;
  3. 解析CSS,构建Render树;(将CSS代码解析成树形的数据结构,与Dom树结合形成Render树)
  4. 布局Render树(Layout/reflow),负责各元素尺寸、位置的计算;
  5. 绘制Render树(painting),绘制页面像素信息;
  6. 浏览器将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上

三、微任务与宏任务

宏任务包括:

  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

微任务包括:

  • process.nextTick(node环境中)
  • Promise
  • Async/Await
  • MutationObserver(html5新特性)

运行机制:

  1. 执行一个宏任务(栈中没有就从事件队列中获取);
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中去;
  3. 宏任务执行完毕后,立即依次执行当前微任务队列的所有微任务;
  4. 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染;
  5. 渲染完毕后,JS引擎线程继续接管,开始下一个宏任务(从事件队列中获取)。

四、实例分析

示例:

console.log('start');
 
var intervalA = setInterval(() => {
  console.log('intervalA');
}, 0);
 
setTimeout(() => {
  console.log('timeout');
 
  clearInterval(intervalA);
}, 0);
 
var intervalB = setInterval(() => {
  console.log('intervalB');
}, 0);
 
var intervalC = setInterval(() => {
  console.log('intervalC');
}, 0);
 
new Promise((resolve, reject) => {
  console.log('promise');
 
  for (var i = 0; i < 10000; ++i) {
    i === 9999 && resolve();
  }
 
  console.log('promise after for-loop');
}).then(() => {
  console.log('promise1');
}).then(() => {
  console.log('promise2');
 
  clearInterval(intervalB);
});
 
new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('promise in timeout');
    resolve();
  });
 
  console.log('promise after timeout');
}).then(() => {
  console.log('promise4');
}).then(() => {
  console.log('promise5');
 
  clearInterval(intervalC);
});
 
Promise.resolve().then(() => {
  console.log('promise3');
});
 
console.log('end');
答案:

start
promise 
promise after for-loop 
promise after timeout
end 
promise1 
promise3 
promise2 
intervalA 
timeout
intervalC
promise in timeout
promise4
promise5

详细讲解分析:

  1. 识别log一般函数方法,输出“start”(1)
  2. 识别intervalA、setTimeout、intervalB、intervalC为特殊的异步方法,依次放入宏任务队列1,并设置了一个 0ms的立即执行标识;
  3. 识别new promise的resolve方法为一般方法,输出“promise”(2)“promise after for-loop”(3)
  4. 识别.then()方法为特殊的异步方法,放入微任务队列1;
  5. 识别new promise的resolve方法里面的setTimeout,放入宏任务队列1,输出“promise after timeout”(4)
  6. 识别promise的.then()方法,放入微任务队列1;
  7. 识别log一般函数方法,输出“end”(5)
  8.  识别微任务队列1,执行.then()方法,输出“promise1”(6),识别.then()方法,将其放入微任务队列1的队尾;
  9. 继续识别微任务队列1,执行promise的.then()方法,输出“promise3”(7),识别微任务队尾,执行.then()方法,输出“promise2”(8)并清除定时器intervalB;
  10. 微任务队列1执行完毕,识别宏任务队列1,识别intervalA,输出“intervalA”(9);识别setTimeout,输出“timeout”(10)并清除定时器intervalA;识别intervalC,输出“intervalC”(11);执行setTimeout,输出“promise in timeout”(12)。宏任务结束;
  11. 识别new promise的resolve方法里面的setTimeout,根据其.then()方法,输出“promise4”(13);识别.then()方法并将其放置微任务队列队尾,执行并输出“promise5”(14)
     

五、最新变化(2023年12月补充)

根据W3C的最新解释:

任务没有优先级,在消息队列中先进先出;但是消息队列有优先级

  • 每个任务都有个任务类型,同一个类型的任务必须在同一个队列,不同类型的任务可以分属于不同的队列。不同的任务队列有不同的优先级。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行
  • 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行(随着浏览器复杂度的提升,W3C不在使用宏队列的说法

在目前chrome的实现中,至少包含了下面的消息队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级【中】
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级【高】
  • 微队列:用户存放需要最快执行的任务,优先级【最高】

六、面试题

如何理解JS的异步?

单线程是异步产生的原因,事件循环是异步的实现方式

JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。渲染主线程承担着诸多工作,渲染页面、执行JS等,如果使用同步的方式,极有可能造成主线程阻塞,从而导致消息队列中其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。

所以浏览器采用异步的方式来避免。具体做法是,当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排列,等待主线程调度执行。

这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。

阐述下JS的运行机制:事件循环

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。

在chrome的源码中,它开启一个不会结束的for循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。

过去把消息队列简单分为宏队列和微队列,这种说法目前已经无法满足复杂的浏览器环境,取而代之的是一种更为灵活多变的处理方式

根据W3C的官方解释,每个任务都有个任务类型,同一个类型的任务必须在同一个队列,不同类型的任务可以分属于不同的队列。不同的任务队列有不同的优先级,在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。但是浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须有限调度执行。

JS中的计时器能做到精确计时吗?为什么?

不能,因为

  1. 计算机硬件没有原子钟,无法做到精确计时
  2. 操作系统的计时函数本身就有少量偏差,由于JS的计时器最终调用的是操作系统的函数,也就携带了这些偏差
  3. 按照W3C的标准,浏览器实现计时器时,如果嵌套层级超过5层,则会带有4ms的最少时间,这样在计时时间少于4ms时又带来了偏差
  4. 受事件循环的影响,计时器的回调函数只能在主线程空闲的时候运行,因此又带来了偏差

七、最新实例分析

function a() {
	console.log('1');
	Promise.resolve().then(function () {
		console.log('2');
	});
}

function delay(duration) {
	var start = Date.now();
	while (Date.now() - start < duration) {}
}

setTimeout(function () {
	console.log('3');
	Promise.resolve().then(a);
}, 0);

delay(1000);

Promise.resolve().then(function () {
	console.log('4');
});

console.log('5');
答案:

5
4
3
1
2

详细解析分析:

  1. 首先浏览器主线程运行
  2. 碰到setTimeout放到延时队列中去
  3. 执行delay函数阻塞渲染1秒中
  4. 碰到Promise.resolve().then(function () {console.log('4');});放到微队列中去
  5. 执行console.log('5');打印出5(1)
  6. 主线程执行完毕,将微队列立即拉入主线程运行,打印出4(2)
  7. 微队列执行完毕,将延时队列拉入主线程运行,打印出3(3)
  8. 碰到Promise.resolve().then(a);放到微队列中去
  9. 主线程执行完毕,将微队列立即拉入主线程运行,打印出1(4)
  10. 碰到Promise.resolve().then(function () {console.log('2');});放到微队列中去
  11. 主线程执行完毕,将微队列立即拉入主线程运行,打印出2(5)

你可能感兴趣的:(JavaScript,javascript,前端,event,loop)