前言
进程VS线程
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位),详细解释就是启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
通俗的讲就是
- 进程是一个工厂,工厂有它的独立资源,工厂之间相互独立
- 线程是工厂中的工人,多个工人协作完成任务,工厂内有一个或多个工人,工人之间共享空间
浏览器基本
当我们打开浏览器后,在浏览器的任务管理器中可以看到
从图中可以看出,最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。
- Browser进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- Renderer进程。默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下,核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中
- GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
- 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
在浏览器的众多进程中,对于我们来说最终要的就是Renderer进程,一个Renderer进程主要包括以下线程
- GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行
- JS引擎线程
- 也可以称为JS内核,主要负责处理Javascript脚本程序,例如V8引擎。Javascript引擎线程理所当然是负责解析Javascript脚本,运行代码
- 事件触发线程
- 当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
- 定时触发器线程
- 浏览器定时计数器并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
- 异步http请求线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。
注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
单线程的JavaScript
我们都知道,JavaScript语言是单线程的,也就是说,在同一时间只能做一件事,这一点和它的用途有关,JavaScript作为浏览器脚本语言,主要用途是与用户互动,以及操作DOM。这也就决定了它只能是单线程,否则会带来很复杂的同步问题。
如果JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?so,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
Javascript单线程任务被分为同步任务(synchronous task)和异步任务(asynchronous task),同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行
看到这里也许会有疑问,JavaScript不是单线程的么,怎么还可以处理异步任务?原因是JavaScript的单线程是指语法层面的单线程,而不是执行层面的单线程,在代码执行层面,还是可以利用处理器的多线程和任务调度系统
基本概念
-
队列(Queue):一种 FIFO(First In, First Out) 的数据结构,它的特点就是 先进先出
-
栈(Stack):是一种 LIFO(Last In, First Out)的数据结构,特点即 后进先出。
-
调用栈(Call Stack):存放待执行的函数的栈,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空比如
const fun1 = () => { console.log( "fun1" ); }; const fun2 = () => { console.log( "fun2" ); fun1(); }; fun2();- 栈空
- 现在执行到一个
fun2,fun2入栈 fun2又调用了fun1,fun1入栈fun1执行完后 出栈- 然后继续执行
fun2,执行完后fun2也 出栈 - 栈空
这个调用栈我们经常会见到,就是在控制台报错的时候,错误信息显示的就是当前时刻调用栈的状态。
-
Event Table:
事件->回调函数对应表,用来存储 JavaScript 中的异步事件 (request, setTimeout, IO等) 及其对应的回调函数的列表 -
Event Queue:
回调函数 队列,所以它也叫Callback Queue,当Event Table中的事件被触发,事件对应的 回调函数 就会被 push 进这个Event Queue,然后等待被执行
JavaScript Evevt Loop
究竟什么是Event Loop呢?先通过一段伪代码了解一下这个概念:
// queen 用作队列的数组
// 先进先出
const queue = [];
let event;
// "永远"执行
while (true) {
if (queue.length > 0) {
// 拿到队列中的下一个事件
event = queue.shift();
try {
event();
}
catch ( e ) {
repotrError( e );
}
}
}
可以看到,有一个用while循环实现的持续运行的循环,循环的每一轮成为一个tick,对于每个tick而言,如果最队列中有等待事件,那么就从队列中在下一个事件并执行,这些事件就是你的回调函数
在JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask),宏任务队列可以有多个,微任务队列只有一个
- MacroTask:script(整体代码)、setTimeout、setInterval、 I/O 操作、UI 渲染、setImmediate(Node&IE10&Edge)
- MicroTask:Process.nextTick(Node)、Promise、Object.observe(废弃)、MutationObserver
下面着重介绍浏览器中的事件循环
浏览器中的事件循环
EventLoop执行流程
- JavaScript 引擎会把整个 script 代码当成一个宏任务执行,并将在setTimeout 和 Promise任务源中注册的回调函数会被放入到不同的任务队列中
- 检查microtask队列是否为空,若有则到3,否则到5
- microtask步骤:取出microtask中的全部任务执行,执行完成到步骤5
- 执行渲染操作,更新界面
- 执行macrotask中的一个任务,执行完成后返回2
总的来说就是
执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(MicroTask)队列是否为空,如果为空的话,就执行Task(宏任务),否则就一次性执行完所有微任务。
每次单个宏任务执行完毕后,检查微任务(MicroTask)队列是否为空,如果不为空的话,会按照FIFO 的规则全部执行完微任务(MicroTask)后,设置微任务(MicroTask)队列为null,然后再执行宏任务,如此循环,直到直到两个队列都清空。
举个例子
console.log( "script start" );
Promise.resolve().then( () => {
console.log( "promise 1" );
setTimeout( () => {
console.log( "setTimeout 1" );
}, 0 );
} ).then( () => {
console.log( "promise 2" );
} );
setTimeout( () => {
console.log( "setTimeout 2" );
Promise.resolve().then( () => {
console.log( "promise 3" );
} );
}, 0 );
console.log( "script end" );
执行流程
-
执行同步代码,将宏任务(
Tasks)和微任务(Microtasks)划分到各自队列中输出
script start,script end -
执行宏任务后,检测到微任务(
MicroTask)队列中不为空,执行promise1,执行完成promise1后,调用promise2.then,放入微任务(MicroTask)队列中,再执行promise2.then,并将setTimeout 1放入MacroTask输出
promise 1,promise 2 -
当微任务(
MicroTask)队列中为空时,执行宏任务(MacroTask),执行setTimeout2 callback,并将promise3放入MicroTask输出:
setTimeout 2 -
再依次执行微任务 和 宏任务队列
输出
promise 3,setTimeout 1
再举个例子
console.log("script start");
async function async1() {
await async2();
console.log("async1");
}
async function async2() {
console.log("async2");
}
new Promise((resolve, reject) => {
console.log("Promise1");
resolve();
}).then(() => {
console.log("then11");
return new Promise((resolve, reject) => {
console.log("Promise2");
resolve();
}).then(() => {
console.log("then21");
}).then(() => {
console.log("then22");
});
}).then(() => {
console.log("then12");
});
async1();
new Promise((resolve, reject) => {
console.log("Promise3");
resolve();
}).then(() => {
console.log("then31");
new Promise((resolve, reject) => {
console.log("Promise4");
resolve();
}).then(() => {
console.log("then41");
}).then(() => {
console.log("then42");
});
}).then(() => {
console.log("then32");
});
setTimeout(() => {
console.log("setTimeout");
}, 0);
console.log("scriptEnd");
这个例子有没有兴趣自己尝试一下输出呢?答案在最下方哦
这个例子看起来比前一个例子稍微复杂一些,仅仅是多了async/await 和Promise的嵌套,但其中的原理还是一样的,但是其中要注意的是最初的版本中,每个await都会创建两个额外的Promise,无论await 右侧是否为promise,由于promise.then每次都是一轮新的microtask,所以async是在2轮microtask之后,第三轮microtask才得以输出,具体说明详见更快的异步函数和 Promise。async/await因为要经过3轮的microtask才能完成await,被认为开销很大,在2018年对这个问题进行了修复,详见Normative: Reduce the number of ticks in async/await
输出
scriptStart Promise1 async2 Promise3 scriptEnd then11 Promise2 async1
then31 Promise4 then21 then41 then32 then22 then42 then12 setTimeout