1、 基本概念
1.1 进程和线程
-
进程 :cpu资源分配的最小单位
一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的demo.exe就是一个进程。
小技巧:windos电脑 ctrl+shift+esc 打开任务管理器就可以看到电脑当前运行的进程。
-
线程 cpu调度的最小单位
进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
个人理解: 把进程比喻为一个工厂,线程是工厂中的流水线。各流水线各司其职,互相协作,共同完成一个共同的目标。
1.2 浏览器是多进程还是单进程的?
回答: 浏览器是多进程的,可以简单地把浏览器的一个tab 页理解为一个进程,但是还有其它进程(比如插件进程)。同时,浏览器也有它自己的优化机制,比方说有五个空白页,这五个空白页会合并成同一个进程。
浏览器至少包含一下四种进程 详见 谷歌官方文档
-
浏览器进程(主进程)
实现浏览器的基本功能,返回按钮,tab页、用户交互,是浏览器的主要进程。
-
插件进程
每种插件都会有一个对应的进程。
- 渲染进程
- 浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响。
- 重点: 渲染进程启动以后,会开启一个渲染主线程,染主线程中会执行js、html、css 代码。
2、渲染主线程的工作方式。
2.1 渲染主线程(Render Main Thread, 以下称为 RMT)
RMT非常的繁忙,有很多任务需要它处理。比如:
-
解析html、css、根据解析的结果进行布局,执行全局js代码。
以60z帧渲染页面(页面每秒刷新60次)
-
执行事件处理的回调函数(用户点击按钮,用户输入数据、用户点击返回按钮)
-
执行计时器的回调。
-
执行网络请求的回调。
-
执行Promise 的回调。
........ 还有很多的任务
2.2 RMT 遇到的难题:
-
单线程还是多线程?
-
浏览器某个任务的过程中,网络请求完成了,同时用户点击了按钮。同时还有一个Promise也完成了,同时计时器的计时也到时间了…。这么多任务等着它执行,我该怎么协调他们有条不紊的工作呢?
-
网络请求或者计时器计时期间,如何处理?一直等待网络请求完成?一直等待计时器?
-
各种任务之间有没有优先级?
2.3 解决问题
问题1: 单线程
不过有一些辅助的线程,比如计时器线程。但js代码的执行一定是单线程的。
问题2: 队列
类似于后端线程池的思想。RMT会维护很多队列,(至少包含宏队列和微队列),每种队列对应一种任务,每种队列具有不同的优先级。用于存放即将需要处理的任务,RMT会开启一个无限循环,在每次任务执行完成的时候,去检查自己维护的这些队列有没有任务,如果有就出队并执行相应的任务,在执行任务的过程中可能又会产生新的任务加入队列(如:定时器的回调函数中promise 完成了)。
注意:宏队列和微队列目前已经不足以满足浏览器复杂的需求了。比如: 事件队列和计时器队列就不是同一个队列。事件队列的优先级高于计时器队列。
各个工作线程会往队列中添加任务:
回答问题3: 不需要等待
异步:代码执行过程中无法立即处理的问题使用异步解决。XHR、Fetch、setTimeOut、setInterval、addEventListener等统称为异步。如果RMT等待这些任务,就会导致RMT进入阻塞 ,表现为页面卡死。计时器由一个单独的线程来完成,只需要在计时器时间到的时候,把回调函数中的任务加入响应的队列等待RMT 来执行即可。 网络也是同理,由专门负责网络请求的模块去处理网络请求,请求完成的时候把回调函数中的任务加入相应的队列即可。
回答问题4: 有优先级
不同的队列具有不同的优先级。常见的队列:计时器队列(宏队列),微队列(Promise.then的回调、MutationObserver)、事件队列(用户点击了按钮)。优先级: 微队列 > 用户事件队列 > 计时器队列 至于为什么是这个优先级,我觉得应该是ECMA 标准规定的。
注意点:
- 每次完成当前的任务,会依次检查优先级高的队列,从中取出任务进行执行。
- 只有当高优先级的任务队列为空,才会执行低优先级的任务。(vip有优先级高)
- 任务一旦执行就不能打断(即使是vip,可以插队, 也得等当前客户的需要完成)
- vip和vip之间也需要排队。
tips记忆方法: 首先微队列优先级最高。 其次,我们重视用户体验,当前任务完成的时候,面临在用户队列和计时器队列二选一,我们优先执行用户点击事件的回调。
基于此,很多问题都能得到有效的回答:
-
Promide.resolve().then() 可以把一个任务加入微队列,setTimeout() 可以把任务加入计时队列,微队列的优先级高于计时队列的优先级。
-
代码层面分析计时不准确的原因:
- 计时时间到了,但是,当前执行的任务是不能被打断的,只有当任务完成才会去检查队列。
- 就算计时时间到了,刚好当前任务执行完毕,可是由于计时器回调的的优先级很低。有很多优先级比他高的队列任务需要执行。