宏任务 —> 微任务 —> requestAnimationFrame —> 重绘/重排 —> requestIdleCallback —> 宏任务 …….
CPU
计算机的核心是 CPU,它承担了所有的计算任务。
它就像一座工厂,时刻在运行。
进程
假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。
背后的含义就是,单个 CPU 一次只能运行一个任务。
进程就好比工厂的车间,它代表 CPU 所能处理的单个任务。进程之间相互独立,任一时刻,CPU 只能运行一个进程,其他 进程处于非运行状态。CPU 使用时间片轮转进度算法,来实现同时运行多个 进程。
CPU 是几核,就可以运行几个进程。
线程
一个车间里,可以有很多工人,他们共享着车间所有资源,协同完成一个任务。线程就好比车间里的工人,一个 进程可以包括多个线程,多个线程共享 进程资源。
CPU、进程、线程之间的关系
在上文,我们已经简单了解 CPU、进程、线程,简单汇总一下
进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)线程是 CPU 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)- 不同进程之间也可以通信,不过代价较大
单线程与多线程,都是指在一个进程内的单和多
浏览器是多进程的
我们已知 CPU、进程、线程之间的关系,对于计算机来说,每一个应用程序都是一个进程,而每一个应用程序都会分别有很多的功能模块,这些功能模块实际上是通过子进程来实现。对于这种子进程的扩展方式,我们可以称这个应用程序是多进程的
而对于浏览器来说,浏览器就是多进程的,我在 Chrome 浏览器中打开了多个 tab,然后打开 windows 控制管理器:
如上图,我们看到一个 Chrome 浏览器启动了好多个进程。
总结一下:
- 浏览器是多进程的。
- 每一个 Tab 页,就是一个独立的进程。
浏览器包含了哪些进程
- 主进程
- 协调控制其他子进程(创建、销毁)
- 浏览器界面显示,用户交互、前进、后退、收藏
- 讲渲染进程得到内存中的 Bitmap,绘制到用户界面上
- 处理不可见操作,网络请求,文件访问等
- 第三方插件进程
- 每种类型的插件对应一个进程,仅当使用该插件时才创建
- GPU 进程
- 用于 3D 绘制等
渲染进程,就是我们说的浏览器内核- 负责页面渲染,脚本执行,事件处理等
- 每个 tab 页一个渲染进程
那么对于普通的前端操作来说,最重要的进程就是 渲染进程,也就是我们常说的浏览器内核
浏览器内核(渲染进程)
渲染进程有哪些线程
而对于渲染进程来说,它当然也是多线程的了,接下来我们来看一下渲染进程包含哪些线程
GUI渲染线程- 负责渲染页面,布局和绘制
- 页面需要重绘和回流时,该线程就会执行
- 与 js 引擎线程互斥,防止渲染结果不可预期
JS引擎线程- 负责处理解析 和 执行 JS 脚本程序(维护一个执行队列 和 内存堆)
- 只有一个 JS 引擎线程(单线程)
- 与 GUI 渲染线程互斥,防止渲染不可预期
事件触发线程- 用来控制事件循环(鼠标点击、setTimeout、ajax等)
- 当事件满足触发条件时,将事件放入到 JS 引擎所在的执行队列中
定时触发器线程- setTimeout 与 setInterval 所在线程
- 定时任务并不收由 JS 引擎计时的,是由定时触发线程来计时的
异步 http 请求线程- 浏览器有一个单独的线程用于处理 AJAX 请求
- 当请求完成时,若有回调函数,通知事件触发线程
当我们了解了渲染进程包括的这些线程后,我们思考两个问题:
- 为什么 JS 是单线程的
- 为什么 GUI 渲染线程 和 JS引擎线程 互斥
为什么 JS 是单线程的
首先是历史原因,在创建 JS 这门语言时,多进程多线程的架构并不流行,硬件支持并不好。
其次是因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。
而且,如果同时操作 DOM,在多线程不加锁的情况下,最终会导致 DOM 渲染的结果不可预期
比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
为什么 GUI 渲染进程 与 JS引擎线程 互斥
这是由于 JS 是可以操作 DOM 的,如果同时修改元素属性并同时渲染界面(即 JS线程和UI线程同时运行),那么渲染线程前后获得的元素就可能不一致了。
因此,为了防止渲染出现不可预期的结果,浏览器设定GUI渲染线程和JS引擎线程为互斥关系。
当JS引擎线程执行时 GUI渲染线程会挂起,GUI 更新则会被保存在一个队列中等待JS引擎线程空闲时立即被执行
从 Event Loop 看 JS 的运行机制
到这里,我们开始进入我们的主题,什么是 Event Loop
先理解一些概念:
- JS 分为同步任务和异步任务
- 同步任务都在 JS 引擎线程上执行,形成一个
执行栈 - 事件触发线程管理一个
任务队列,异步任务触发条件达成,将回调事件放到任务队列中 执行栈中所有同步任务执行完毕,此时 JS 引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行

我们写setTimeout/setInterval和XHR/fetch代码时,这些代码本身是同步任务,而他们的回调函数才是异步任务。
当代码执行到
setTimeout/setTInterval,实际上是JS引擎线程通知定时触发器线程,间隔一个时间后,会触发一个回调事件,而定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列中当代码执行到
XHR/fetch时,实际上是JS引擎线程通知异步http请求线程,发送一个网络请求,并制定请求完成后的回调事件,而异步http请求线程在接受到收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中
当我们的同步任务执行完,JS引擎线程会询问事件触发线程,在事件队列中是否有待执行的回调函数,如果有就会加入到执行栈中交给JS引擎线程执行

总结一下:
- JS引擎线程只执行执行栈中的事件
- 执行栈中的代码执行完毕,就会读取事件队列中的事件
- 事件队列中的回调事件,是由各自线程插入到事件队列中的
- 如此循环
宏任务、微任务
什么是宏任务
我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他。
我们前文提到过JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个GUI渲染线程开始工作,对页面进行渲染
宏任务 —> 渲染 —> 宏任务 —> 渲染 —> ….
宏任务包含:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI Render
第一个例子
document.body.style.background = 'red';document.body.style.background = 'blue';document.body.style.background = 'aqua';
效果如下:
页面背景在瞬间变成蓝色,以上代码属于同一次的宏任务,所以全部执行完才触发页面渲染,渲染时GUI线程会将所有 UI 改动优化合并,所以视觉效果上,只会看到页面变成蓝色
第二个例子
document.body.style.background = 'red';setTimeout(function () {document.body.style.background = 'aqua';}, 500)
效果如下
页面先是红色,然后是蓝色,因为以上代码是两次宏任务,第一次宏任务执行的代码把背景变红,然后触发渲染,第二次宏任务把背景变蓝。
什么是微任务
我们已经知道宏任务结束后,会执行渲染,然后执行下一个宏任务,而微任务可以理解成在当前宏任务执行后,立即执行的任务,然后才进行渲染。
也就是说,当宏任务执行完后,会在渲染前,将执行期间产生的所有微任务都执行完。
宏任务 —> 微任务 —> 渲染 —> 宏任务 —> 微任务 —> 渲染 —> …
微任务包含:Promise的回调,MutationObserver

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

控制台输出 1 3 2,是因为 promise 对象的 then 方法的回调是异步执行,所以 2 最后输出
页面的背景色直接变成黑色,没有经过蓝色的阶段,是因为,我们在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务回调,在微任务回调中,将背景变成黑色,然后才执行渲染。
第二个例子
const fn1 = () => {console.log(1)Promise.resolve(3).then(data => console.log(data))}const fn2 = () => console.log(2);setTimeout(fn1, 0)setTimeout(fn2, 0)// print : 1 3 2
一开始,执行栈中有两个 setTimeout,代码执行后,fn1、fn2分别被放入到事件队列。
由于执行栈已经空了,把 fn1放入 执行栈并执行,打印出了 1,还执行了promise ,并将 promise 回调存到微任务队列中。
现在执行栈为空,事件队列中还有 fn2,微任务队列还有promise 回调,讲道理,微任务队列先执行,所以 打印出 3
最后,打印出 2
总结 Event Loop 过程
- 一开始,执行一个
宏任务(栈没有就从事件队列中获取) - 执行过程中如果遇到
微任务,就把微任务放入微任务队列 宏任务执行完毕后,立即执行当前微任务队列中所有微任务(依次执行)- 当前
宏任务执行完毕,开始检查渲染,然后GUI 线程接管渲染 - 渲染完毕后,
JS 线程继续接管,开始下一个宏任务(从事件队列中获取)

练习题
题(1)
console.log(1);// timer-1setTimeout(() => {console.log(2);Promise.resolve().then(() => {// then-1console.log(3);})});new Promise((resolve, rejcet) => {// promise-1console.log(4);resolve(5);}).then(res => {// then-2console.log(res);})// timer-2setTimeout(() => {console.log(6);});console.log(7);
结果: 1 -> 4 -> 7 -> 5 -> 2 -> 3 -> 6
题(2)
console.log('script start');async function async1() {await async2();console.log('async1 start');}async function async2() {console.log('async2 end');}async1();setTimeout(() => {console.log('setTimeout');}, 0);new Promise(resolve => {console.log('Promise');resolve()}).then(() => {console.log('promise1');}).then(() => {console.log('promise2');})console.log('script end');
答案:script start -> async2 end -> Promise -> script end -> async1 start -> promise1 -> promise2 -> setTimeout
因为 async-await 就是 Promise + generator 的一种语法糖而已,因此上述代码可以经过转换得到如下代码:
console.log('script start');function async1() {// await 可以换成 promise.then, 后续代码嵌入 then 内async2().then(() => {console.log('async1 start');});}async function async2() {console.log('async2 end');}async1();setTimeout(() => {console.log('setTimeout');}, 0);new Promise(resolve => {console.log('Promise');resolve()}).then(() => {console.log('promise1');}).then(() => {console.log('promise2');})console.log('script end');
await 在执行 async 函数时是会有阻塞性的,所以 async end 出现在 Promise 之前。
