一 进程与线程
进程和线程的概念用较为官方的术语描述来说是这样的
1.进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
2.线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
用LOL来比喻的话(举例子可能不太贴切,因为本人比较喜欢玩lol 所以用这个举例子让自己来加深印象)
一个进程就是一局游戏,每一局游戏都要有野怪小兵这些资源;
每局游戏都相互独立互不干涉;
一个线程就是一个英雄;
一局游戏里有多个英雄(一个进程有多个线程);
英雄之间共享资源
映射关系为
一局游戏的野怪和小兵 > 系统分配的内存
每一局游戏相互独立 > 进程之间相互独立
打团战 -> 多个线程在进程中协作完成任务
一局游戏可能一个玩也可能多个人玩>一个进程由一个或多个线程组成
英雄之间共享野怪小兵谁都可以吃 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
二 浏览器是多进程的
浏览器可以运行是因为系统给分配了资源和内存,浏览器每一个tab页都需要资源和内存,所以每一个tab页面都对应至少一个进程,在这里浏览器应该也有自己的优化机制,有时候打开多个tab页后,可以在Chrome任务管理器中看到,有些进程被合并了 (所以每一个Tab标签对应一个进程并不一定是绝对的)
知道了浏览器是多进程后,再来看看它到底包含哪些进程:(为了简化理解,仅列举主要进程)
1. Browser进程
浏览器的主进程(负责协调、主控),只有一个。
负责浏览器界面显示,与用户交互。如前进,后退等
负责各个页面的管理,创建和销毁其他进程
将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
网络资源的管理,下载等
2. 第三方插件进程
每种类型的插件对应一个进程,仅当使用该插件时才创建
3. GPU进程
最多一个,用于3D绘制等
4. 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的)
默认每个Tab页面一个进程,互不影响。
主要作用为
页面渲染,脚本执行,事件处理等
三 浏览器渲染进程
作为前端开发工程师我们最主要的关注点还是渲染进程。
可以这样理解,页面的渲染,JS的执行,事件的循环,都在这个进程内进行,渲染进程是多线程的
接下来重点分析这个进程的常驻线程。
1.GUI渲染进程
1.负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
2.当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。
3.注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
2.JS引擎线程
1.也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
2.JS引擎线程负责解析Javascript脚本,运行代码。
3.JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
4.同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
3.事件处理线程
1.归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
2.当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
3.当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
4.注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
4.定时器线程
1.传说中的setInterval与setTimeout所在线程
2.浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
3.因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
4.注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
5.http异步请求线程
1.在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
2.将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
四 事件循环
上文说了几个线程
js引擎线程
事件触发线程
定时触发器线程
然后再理解一个概念:
JS分为同步任务和异步任务
1.同步任务都在主线程上执行,形成一个执行栈
2.主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件
3.一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行
主线程运行时会产生执行栈,栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)
而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
如此循环
注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件
五 宏任务微任务
上文中将JS事件循环机制梳理了一遍,在ES5的情况是够用了,但是在ES6盛行的现在,仍然会遇到一些问题,譬如下面这题:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
它的正确执行顺序是这样子的:
script start
script end
promise1
promise2
setTimeout
为什么呢?因为Promise里有了一个一个新的概念:microtask。微任务
JS中分为两种任务类型:macrotask和microtask,在ECMAScript中,microtask称为jobs,macrotask可称为task。
它们的定义?区别?简单点可以按如下理解:
macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
每一个task会从头到尾将这个任务执行完毕,不会执行其它
浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务
也就是说,在当前task任务后,下一个task之前,在渲染之前
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)
分别很么样的场景会形成macrotask和microtask呢?
macrotask:主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask)
microtask:Promise,process.nextTick等
再根据线程来理解下:
macrotask中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护
microtask中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由JS引擎线程维护(这点由自己理解+推测得出,因为它是在主线程下无缝执行的)
所以,总结下运行机制:
执行一个宏任务(栈中没有就从事件队列中获取)
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
最后附上几道题
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
})
console.log('script end');
console.log('1');
setTimeout(function() {
console.log('2');
new Promise(function(resolve) {
console.log('3');
resolve();
}).then(function() {
console.log('4');
})
})
new Promise(function(resolve) {
console.log('5');
resolve();
}).then(function() {
console.log('6');
})
setTimeout(function() {
console.log('7');
})
setTimeout(function() {
console.log('8');
new Promise(function(resolve) {
console.log('9');
resolve();
}).then(function() {
console.log('10');
})
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12');
})
console.log('13');
const promise = new Promise((resolve, reject) => {
console.log("1");
setTimeout(() => {
console.log("2");
setTimeout(() => {
console.log('4');
})
resolve('success');
}, 1000)
})
console.log('3');
promise.then((res) => {
return new Error('error!!!')
}).then((res) => {
console.log('then:', res)
}).catch((err) => {
console.log('catch:', err)
})
promise.then((res) => {
console.log(res)
}).catch((err) => {
console.log('catch:', err);
})
欢迎大家在评论区写出执行顺序