宏任务 —> 微任务 —> 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-1
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
// then-1
console.log(3);
})
});
new Promise((resolve, rejcet) => {
// promise-1
console.log(4);
resolve(5);
}).then(res => {
// then-2
console.log(res);
})
// timer-2
setTimeout(() => {
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
之前。