JS运行机制
开局一问
下列代码输出顺序是否能答对呢?能答对是否是知道为什么呢?不知道的话跟着我往下看。
function test() {
console.log(1)
setTimeout(function () { // timer1
console.log(2)
}, 1000)
}
test();
setTimeout(function () { // timer2
console.log(3)
})
new Promise(function (resolve) {
console.log(4)
setTimeout(function () { // timer3
console.log(5)
}, 100)
resolve()
}).then(function () {
setTimeout(function () { // timer4
console.log(6)
}, 0)
console.log(7)
})
console.log(8)
// 输出1,4,8,7,3,6,5,2
进程和线程
CPU是计算机的核心,承担所有的计算任务
1.1 什么是进程?
官网说法:进程是CPU资源分配的最小单位。
字面意思就是进行中的程序,可以理解为一个独立运行且用于偶自己的资源空间的任务程序,进程包括运行中的程序和程序所用到的内存子资源。
CPU可以拥有很多进程,打开一个软件就有一个或多个进程,电脑运行很多个软件就会卡,是因为CPU需要给每个进程分配内存资源空间,内存有限,进程越多就越卡。
例如:浏览器打开一个tab相当于一个进程
1.2 什么是线程?
线程是CPU调度的最小单位
线程是建立在进程基础上的一次程序运行单位,一个进程可以有多个线程。
一个进程由一个或多个线程组成,线程可以理解为是一个进程中代码的不同执行路线。
一个进程中只有一个执行流称作为单线程,即程序执行时,路线是按顺序排好的,前面的处理完,才能执行后面的。
一个进程中有多个执行流称作为多线程,即程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
调度和切换:线程上下文切换比进程上下文切换要快得多。
1.3 浏览器
1.3.1 浏览器包含哪些进程?
- Browser进程
- 浏览器的主进程——负责协调、主控
- 负责浏览器页面显示
- 负责各个页面的管理
- 网络资源的管理、下载等
- 第三方插件进程
- 每种类型的插件对应一个进程,当使用插件的时候创建
- GPU进程 —— 用于3D绘制等
- renderer渲染进程
- 通常说的浏览器内核
- 每个Tab页面都有一个渲染进程
- 主要作用为页面渲染、脚本执行、事件处理等
假设浏览器是单进程,那么某一个Tab或者第三方插件崩溃了,就影响整个浏览器。
1.3.2 渲染进程
页面的渲染,JS的执行,事件的循环,都在渲染进程内执行,所以我们要重点了解渲染进程
渲染进程是多线程的,看渲染进程的一些常用较为主要的线程:
GUI渲染线程
- 负责渲染浏览器页面,解析HTML、CSS,构建DOM树和RenderObject树,布局和绘制等
- 修改了一些元素的颜色或者背景色,页面就会重绘
- 修改了元素的尺寸,页面就会回流
- 当页面需要重绘和回流的时候,GUI线程执行绘制页面
- 回流的成本比重绘的成本高,尽量避免这两种情况
- GUI渲染线程和JS引擎线程是互斥的
- JS引擎执行的时候GUI会被挂起
- GUI更新保存在一个队列中,等到JS引擎空闲的时候立即执行
JS引擎线程
- JS引擎就是JS内核,负责解析和运行JS脚本程序
- 一直等待这任务队列中任务的到来,然后加以处理
- 浏览器同时只能有一个JS引擎在运行JS程序,所以Js是单线程运行的
- 一个Tab页中无论什么时候只有一个JS线程运行JS程序
- JS引擎会堵塞GUI渲染线程
- JS运行时间过长,造成页面渲染不连贯,导致页面渲染加载堵塞
- GUI解析HTML的时候,遇到了script标签,就会停止GUI 的渲染,先执行js代码,执行完后,GUI继续渲染;代码执行时间过长会导致页面卡顿,因此才有了(defer 和 async)
事件触发线程
- 属于浏览器,而不是jS引擎,用来控制事件循环,并且管理着一个事件队列
- 当js执行遇到事件绑定和异步操作,会走事件触发线程将对应的时间添加到对应的线程中,等异步事件有了结果,将他们的回调添加到事件队列中,等待js引擎空闲时来处理
- 当对应的事件触发时,该线程会把时间添加到带处理队列队尾,等待js执行
- 因为JS是单线程,所以这些待处理队列中的事件都得排队等待
定时器的线程
- setInterval与setTimeout所在线程
- 浏览器定时计数器不是由JS引擎计数的(因为堵塞就会影响计时的准确性)
- 通过单线程来计时并触发定时,计时结束后将回调事件放入到事件触发线程的时间队列中
- W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
异步的HTTP请求的线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中
事件循环(Event Loop)基础
JS分为同步任务和异步任务。同步任务都在主线程(这里的主线程就是JS引擎线程)上执行,会形成一个执行栈。
事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放一个事件回调。一旦执行栈中的所有同步任务执行完毕(也就是JS引擎线程空闲了),系统就会读取任务队列,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中。
反复执行,就是我们所谓的事件循环。
宏任务& 微任务
由于JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染
宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成在当前宏任务执行后立即执行的任务。
1. 宏任务
可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他。
常见的宏任务(一般记住这几个宏任务,其他基本上都是微任务):
- 主代码块;
- setTimeout;
- setInterval;
- setImmediate () -NodeJS;
- requestAnimationFrame () -浏览器
2. 微任务
微任务可以理解成在当前宏任务执行后立即执行的任务。
常见微任务
- process.nextTick () -Node;
- Promise.then();
- catch;
- finally;
- Object.observe;
- MutationObserver;
注意:new Promise(() => {}).then() 中,前面的 new Promise() 这一部分是一个构造函数,这是一个同步任务,后面的 .then() 才是一个异步微任务
async/await
在使用await关键字与Promise.then效果类似,await 以前的代码,相当于与 new Promise 的同步代码,await 以后的代码相当于 Promise.then的异步
setTimeout(() => console.log(4))
async function test() {
console.log(1)
await Promise.resolve() // 这一行以及同作用域下后面代码 类似于 then的回调,属于微任务
console.log(3)
}
test()
console.log(2)
// 输出1 2 3 4
3. 事件循环进阶版
开局问题解析
通过这一系列的概念解析,开局问题应该大致有了思路吧,下面看看是不是和你想的一样吧。
因为JS是从上到下执行的
- 执行同步代码test(),遇到console.log(1), 因此打印 1
- 遇到setTimeout定时为1000ms,因此1000ms后将回调放入事件触发器的任务队列(宏任务)
- 执行完test(),再次遇到setTimeout,这个没有写延迟时间,因此为默认的0,将回调放入宏任务队列
- 接着遇到了Promise,因为Promise中代码为同步代码,因此执行console.log(4),打印4;执行setTimeout延迟为100ms,因此100ms后将回调放入宏任务队列
- Promise.then是微任务,因此将then的回调放入微任务队列中
- then之后,遇到同步代码console.log(8),直接打印8;至此同步任务执行完毕;开始执行微任务
- 微任务队列中只有一个Promise.then的回调函数,执行回调,遇到了setTimeout延迟为0,将回调放入宏任务队列;然后执行同步代码console.log(7),因此直接打印7;微任务队列执行完毕
- 开始执行,宏任务队列,根据setTimeout的延迟时间可得到宏任务队列中的回调函数顺序为console.log(3)、console.log(6)、console.log(5)、console.log(2);所以按顺序打印为 3,6,5,2