多线程的浏览器、单线程的JavaScript和Event Loop

我们经常看到这样的说法,浏览器是多线程的,而js是单线程的,以及事件队列(event loop)和异步回调的概念,现在总结下多线程的浏览器和单线程的JavaScript。

CPU
cpu是整个计算机的核心,一直在运行,完成计算机的各种计算任务,相当于一个工厂。

进程
进程可以看作工厂里的车间。工厂有很多车间,CPU就有很多进程。多个车间可以同时工作,多个进程也可以互不干扰独立完成工作。

线程
线程可以看做车间里的工人。一个车间有很多个工人,使用相同的资源,协作完成任务。一个进程有多个线程,多个线程共享资源,协助或者单独完成任务。

CPU、进程和线程的关系

  • 进程是CPU进行资源分配和独立运行的最小单元
  • 线程是系统独立调度和分派的基本单元
  • 单线程和多进程是指在一个进程内的单和多。

多线程的浏览器
浏览器是多进程的,每打开一个tab页,就是开启了一个进程。我们打开多个多个tab后,在启动windows里的任务管理器或者mac的活动监视器,可以看到一个chrome中有多个线程。
多线程的浏览器、单线程的JavaScript和Event Loop_第1张图片
浏览器中的进程
浏览器中包含以下进程

1. Browser Process

  • 浏览器的主进程,负责协调其他子进程。
  • 负责浏览器界面的显示,包括地址栏、前进、后退等按钮的工作。
  • 负责不可见的底层操作,如网络请求、文件访等。
  • 负责页面的管理,创建和销毁其他进程

2 Renderer Process

  • 负责一个tab页的所有事情,包括脚本解析、执行、页面渲染等。

3 Plugin Process

  • 浏览器使用的插件,每个类型的插件对应一个进程,使用该插件时才会创建一个进程

4 GPU Process

  • 负责GPU相关的事务,如3D绘制等。

我们开启一个tab后,打开chrome的任务管理,可以看到当前浏览器运行的所有进程,以及每个进程占用的资源。
多线程的浏览器、单线程的JavaScript和Event Loop_第2张图片

多线程的浏览器
如上文所述,线程和进程是多对一的关系,一个进程内可以有多个线程。对Renderer Process 来说,也有很多个线程,常见的线程如下:
多线程的浏览器、单线程的JavaScript和Event Loop_第3张图片

1. GUI渲染线程

  • 负责浏览器页面HTML的渲染和绘制
  • 如果页面需要重排或者回流,该线程会执行
  • 与js引擎线程互斥,在js引擎线程执行时,该线程会停止执行

2. JS引擎线程

  • 负责解析和实行JavaScript脚本,是主线程
  • 一个tab只有一个JS引擎线程
  • 与GUI渲染线程互斥,如果js解析时间比较长,GUI渲染线程处于等待的状态,会造成页面卡顿

3. 事件触发线程

  • 当js引擎线程执行到绑定的事件时,该线程会把绑定事件添加到该线程
  • 等到被绑定的事件触发时,该线程会把触发事件后的回调函数添加到任务队列,等待JS引擎线程处理
  • 被触发的事件可以是鼠标点击、ajax异步等
  • 由于js单线程,故所有的事件都得排队等JS引擎线程处理

4. 定时触发器线程

  • 定时器setTimeout、setInterval所在的线程
  • js是单线程的,如果计时器在主线程计时,会阻碍线程,因此需要别的线程专门处理计时
  • 当JS引擎线程执行到setTimeout、setInterval关键词时,会把定时器任务添加到该线程,等计时完毕后,通知事件触发线程

5. 异步HTTP请求线程

  • 一个处理ajax的线程
  • 请求完成时,如果有回调函数,通知事件触发线程

浏览器线程间的关系
GUI渲染线程和 JS引擎线程是互斥的。当JS引擎线程执行时,GUI渲染线程被挂起,GUI的更新会保存到队列中,等JS引擎线程线程空闲时执行。
若GUI渲染线程和 JS引擎线程同时执行,假设JS引擎线程更改了dom结构,GUI渲染线程进行了渲染,那么 GUI渲染线程获取到的dom结构可能和JS引擎线程操作dom后的结构不一致。为了防止这种现象,js被设计成单线程的,即GUI渲染线程和 JS引擎线程不可同时执行。

事件触发线程和定时触发器线程、异步HTTP请求线程的共同点在于在于他们都使用了回调函数。当触发回调函数时,浏览器会将回调函数放到事件队列里,等JS引擎线程空闲时执行。

重新看待Event Loop
对于Event Loop我们知道以下几点常见的说法

  • js有异步任务和同步任务
  • 同步任务都在JS引擎上执行,有一个执行栈
  • 事件触发引擎有一个事件队列,回调函数被放到此队列中。
  • 执行栈中的所有任务完成后,如果JS引擎是空闲的,则会读取事件队列中的任务,将可执行的回调事件添加到执行栈中执行。
    多线程的浏览器、单线程的JavaScript和Event Loop_第4张图片
    举个例子,当JS引擎线程执行到ajax时,会通知异步http请求线程发送请求,并注册监听的回调函数。待请求返回后,会将回调函数添加到事件触发线程管理的事件队列中。
    当JS引擎线程执行到setTimeout或setInterval时,JS引擎线程通知定时触发器线程计时,定时触发器线程计时完毕后,把回调函数放入到事件触发线程管理的事件队列。
    因此,JS引擎线程在执行完同步任务后,会问事件触发线程是否有待执行的回调函数,如果有则执行。
    由此可见,setTimeout/setInterval/ajax的执行是同步的,但其中的回调函数是异步的。

多线程的浏览器、单线程的JavaScript和Event Loop_第5张图片

宏任务和微任务
我们可以把执行栈执行的代码当做执行宏任务,在宏任务执行时,不会去执行别的任务。
如上所述,JS引擎线程和GUI渲染线程是互斥的。浏览器为了让宏任务和微任务有序进行,会在两个宏任务执行间隔中执行GUI渲染线程。

宏任务–>渲染–>宏任务–>渲染–>渲染...

js引擎线程中的代码和setTimeout、setInterval等,都属于宏任务,只不过它们可能属于不同的事件队列。

浏览器在宏任务执行完毕,GUI渲染线程执行前,执行微任务。

document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
    console.log(2);
    document.body.style = 'background:black'
});
console.log(3);

以上代码输出132,并且跳过蓝色阶段,直接将背景色变为黑色。这是因为这段代码属于同一个宏任务队列,待同步任务执行完毕执行微任务,then中的回调函数属于微任务,直接将背景色设置合为黑色,然后GUI渲染线程开始渲染。

因此总结如下:

  1. 执行一个宏任务(栈中没有就从事件队列中获取)
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  3. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  4. 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  5. 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
    多线程的浏览器、单线程的JavaScript和Event Loop_第6张图片

你可能感兴趣的:(javascript)