为什么页面会闪烁?异步任务是如何在单线程的js中运行的?这一切都跟浏览器的运行机制有关系。为了更好的开发页面,我初步梳理了浏览器的运行机制,请大家一起学习,批评指正。
进程与线程
CPU
是计算机的核心,承担所有的计算任务。
进程
是CPU
的最小资源分配单位,进程
包括运行中的程序和程序所使用到的内存和系统资源。
线程
是CPU
调度的最小单位,线程
就是程序中的一个执行流,一个进程
可以有多个线程
。一个进程下只有一个线程,称之为单线程
,一个进程
下有多个线程
,称之为多线程
。
单线程与多线程
进程
之间相互独立,同一进程
下的各个线程
间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)。
也就是说,同一个进程下,多个线程是共享的数据,线程A、线程B可操作同一个对象。这时候对于浏览器页面DOM
来说,如果两个线程同时操作一个DOM节点
,一个要删除,一个要添加,那么就不知道该以哪个为准。于是浏览器在与DOM
相关的页面脚本执行与UI渲染方面,选择了单线程的执行方式。
浏览器的运行机制
浏览器是多进程的,例如Chrome,我们每打开一个Tab页就会产生一个进程,我们打开很多标签页不关,电脑会越来越卡,很耗CPU。浏览器包含这些类型的进程:
- 浏览器的主进程(负责协调、主控),该进程只有一个。
- 负责浏览器界面显示,与用户交互。如前进,后退。
- 负责各个页面的管理,创建和销毁其他进程。
- 网络资源的管理,如下载。
- 第三方插件进程,每种类型的插件对应一个进程,当使用该插件时才创建。
- GPU进程,该进程也只有一个,用于3D绘制。
- 页面渲染进程,即通常所说的
浏览器内核
。- 每开一个Tab页面都会产生一个
渲染进程
,进程间互相独立。 - 主要作用为
页面渲染
,脚本执行
,事件处理
等。- 进程内部是
多线程
,包括GUI渲染线程
,js引擎线程
,事件处理线程
,定时器线程
,异步http请求线程
等。 - 其中,
事件处理线程
,定时器线程
,异步http请求线程
是可以和其他线程并发执行的,并且,这些类型的线程每种都可以有多个。 GUI渲染线程
,js引擎线程
是两个单线程类型线程,他们在一个进程内,都只能存在一个。并且他们是互斥的,只能一个线程执行完一个tick之后,才会执行另一个线程。这是因为他们都与DOM
相关,而DOM
的变化必须只能以一个线程为准。所以有的时候,js执行过慢会让页面渲染卡顿。- js引擎线程是一个单线程(主线程是单线程,可以开启多个子线程如:
WebWorker
,子线程完全受主线程控制且不可操作DOM
),就是JS内核,负责处理Javascript脚本程序(例如V8引擎)。他拥有计算能力,可以执行js脚本的计算逻辑,还可以进行DOM
操作,还可以调用或者绑定页面进程的其他线程接口的异步服务,例如异步http请求
、计时器
、浏览器事件
。
- 进程内部是
- 每开一个Tab页面都会产生一个
js引擎线程
js引擎线程
,他拥有一个执行栈
,以及一个宏任务
队列和一个微任务
队列。
- 首先由
执行栈
执行脚本本身的同步代码,遇到异步操作
就调用异步任务相关的线程(例如定时器、http请求等)去执行并等待有回调结果放入任务队列
,或者直接放入任务队列
。 - 这些
异步操作
,有些会产生一个微任务
的回调,有些会产生一个宏任务
的回调。所以任务队列
有两个,一个宏任务队列
,一个微任务队列
。 - 当
执行栈
所有同步代码都执行完毕,就会先查看微任务
队列,清空微任务队列后,一个tick(Event Loop - 事件循环)完成。 - 每当一个tick完成,就会让位于
GUI渲染线程
,如果有等待渲染的任务,就进行一次UI渲染,没有就从宏任务队列
轮询宏任务,继续执行下一次宏任务。
Event Loop
以下是完整的Event Loop运行机制
宏任务与微任务
宏任务
在ECMAScript中,宏任务被称为macrotask
也被称为task
。
常见的宏任务
- 主代码块
- setTimeout
- setInterval
- setImmediate (Node)
- requestAnimationFrame (浏览器)
微任务
在ECMAScript中,微任务被称为microtask
也被称为jobs
。
ES6新引入了
Promise
标准,同时浏览器实现上多了一个microtask
微任务概念。我们已经知道宏任务结束后,会执行UI渲染,然后执行下一个宏任务,而微任务可以理解成在“当前宏任务执行后立即执行的任务”
。当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完。
常见的微任务
- process.nextTick (Node)
- Promise.then
- catch
- finally
- Object.observe
- MutationObserver
Promise
new Promise()
本身是一个构造函数,里面的匿名函数是同步任务
,将会同步执行。而后面的then
、catch
是异步的微任务
。
new Promise(resolve => {
console.log(1)
resolve()
}).then(()=>{
console.log(3)
})
console.log(2)
// 输出结果:1,2,3
async/await
async/await本质上基于Promise和Generator的语法糖
,而Promise是属于微任务的一种。所以await关键字与Promise.then效果类似,他相当于把执行await的当行代码,包装成一个Promise
对象去执行,所以await当行代码会立即执行。而await
后面的代码块,会被打包放在一个then
的回调函数里。所以后面的代码块变成了异步微任务,不在立即执行,而是被执行栈放到微任务队列
后,就继续执行函数外面的同步代码。
// 工具函数
async function print (i) {
console.log(i);
}
// 测试函数
async function test() {
console.log(2);
await print(3);
console.log(6);
}
// 开始执行
console.log(1);
test();
new Promise(resolve => {
console.log(4);
resolve();
}).then(() => {
console.log(7);
})
console.log(5);
// 输出结果:1,2,3,4,5,6,7
node环境的js引擎运行机制
虽然NodeJS中的JavaScript运行环境也是V8,也是单线程,但是,还是有一些与浏览器环境中的表现是不一样的。区别就是nodejs的宏任务分好几种类型,而这好几种又有不同的任务队列,而不同的任务队列又有顺序区别,而微任务是穿插在每一种宏任务之间的。所以node的任务机制更为复杂。
而浏览器
只有一个宏任务队列
和一个微任务队列
,相对简单清晰。
参考
[3]8张图让你一步步看清 async/await 和 promise 的执行顺序
[4]promise、async/await在任务队列中的执行顺序
[5]并发模型与事件循环