关于浏览器事件循环的基础知识
node相关基础知识点
可以看这篇:node面试题
进程和线程
-
进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。
-
线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。
更多可以看这篇:进程、线程、虚拟内存、内核模式和用户模式
进程间通信的方式
参考答案
1.管道pipe:半双工的通信方式,数据只能单向流动,只能在父子进程间使用。
2.命名管道FIFO:半双工的通信方式,但是它允许无亲缘关系进程间的通信。
3.消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
4.共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
5.信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
6.套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
7.信号(sinal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
浏览器的多进程架构
浏览器的主要进程和职责(如图):
-
主进程 Browser Process
- 负责浏览器界面的显示与交互。各个页面的管理,创建和销毁其他进程。网络的资源管理、下载等。
-
第三方插件进程 Plugin Process
- 每种类型的插件对应一个进程,仅当使用该插件时才创建。
-
GPU 进程 GPU Process
- 最多只有一个,用于 3D 绘制等
-
渲染进程 Renderer Process
- 称为浏览器渲染进程或浏览器内核,内部是多线程的。主要负责页面渲染,脚本执行,事件处理等。(本文重点分析)
浏览器内核
浏览器是多进程的,浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。浏览器内核有多种线程在工作:
- GUI渲染线程:
- 负责渲染页面,解析HTML,CSS构成DOM树等,当页面重绘或回流时调用该线程。
- 和JS引擎线程互斥,当JS引擎线程在工作的时候,GUI渲染线程会被挂起,GUI更新被放入在JS任务队列中,等待JS引擎线程空闲的时候继续执行。
- JS引擎线程:
- 单线程工作,负责解析运行javascript脚本。
- 和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
- 事件触发线程:
- 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
- 定时器触发线程:
- 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
- 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待JS引擎处理。
- http请求线程:
- http 请求的时候会开启一条请求线程。
- 请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。
JS事件循环
- 为什么?
- JavaScript 引擎是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。如果某个执行时间太长,就容易造成阻塞;为了解决这一问题,JavaScript引入了事件循环机制。
- 是什么?
- JavaScript 单线程中的任务分为同步任务和异步任务。从下面流程图中可以看到,主线程不断从任务队列中读取事件,这个过程是循环不断的,这种运行机制就叫做Event Loop(事件循环)!
- JavaScript 单线程中的任务分为同步任务和异步任务。从下面流程图中可以看到,主线程不断从任务队列中读取事件,这个过程是循环不断的,这种运行机制就叫做Event Loop(事件循环)!
JS事件循环机制
JavaScript事件循环机制分为浏览器和Node事件循环机制,这里主要讲的是浏览器部分。
Javascript 有一个main thread主线程和call-stack调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。这里先了解JS执行环境及作用域,可以看这篇:浅谈JS执行环境和作用域。
- JS调用栈
- JS 调用栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。
- 同步任务、异步任务
- JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
- Event loop
- 调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。
- 调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。
事件循环下的宏任务和微任务
通常我们把异步任务分为宏任务与微任务,它们的区分在于:
- 宏任务(macro-task):一般是JS引擎和宿主环境发生通信产生的回调任务,比如 setTimeout,setInterval是浏览器进行计时的,其中回调函数的执行时间需要浏览器通知到JS引擎,网络模块,I/O处理的通信回调也是。整体script代码、setTimeout、setInterval、setImmediate、DOM事件回调,ajax请求结束后的回调都是。执行顺序:整体script代码>setTimeout>setInterval>setImmediate
- 微任务(micro-task):一般是宏任务在线程中执行时产生的回调。比如process.nextTick、Promise、Object.observe(已废弃), MutationObserver(DOM监听),都是JS引擎自身可以监听到的回调。执行顺序:process.nextTick>Promise>Object.observe
在事件循环中,任务一般都是由宏任务开始执行的(JS代码的加载执行),在宏任务的执行过程中,可能会产生新的宏任务和微任务,这时微任务会被添加到当前任务队列,当前宏任务执行完,会执行任务队列中的微任务,当前队列的微任务执行完毕,再执行新的宏任务。
典型案例详细解析
- 案例说明一
let promise = new Promise((res,rej)=>{
setTimeout(()=>{
console.log(0)
},0)
console.log(1)
res();
rej();
})
promise.then(()=>{
console.log(2)
},()=>{
console.log(3)
}
)
// 输出 1 -- 2 --0 状态一旦更改不会再改变
- 案例说明二
setImmediate(function(){
console.log(1);
},0);
setTimeout(function(){
console.log(2);
},0);
new Promise(function(resolve){
console.log(3);
resolve();
console.log(4);
}).then(function(){
console.log(5);
});
console.log(6);
process.nextTick(function(){
console.log(7);
});
console.log(8);
根据js的运行原理解释上面代码的执行顺序:
代码都是从script主程序开始执行;
创建setImmediate宏任务;
创建setTimeout宏任务;
执行到Promise,输出3、4,当前宏任务(script主程序)的程序还没有执行完,创建微任务Promise.then的回调;
执行console.log(6)输出6;
创建微任务process.nextTick;
执行console.log(8)输出8;
执行任务队列中的微任务,按照优先级:process.nextTick > Promise.then 依次输出 7,5;
微任务执行完毕,执行宏任务,按照优先级: setTimeout > setImmediate 依次输出2,1
// 3 4 6 8 7 5 2 1
- 案例说明三
setTimeout(function(){
console.log(1);
},0)
async function async1(){
console.log(2);
await async2();
console.log(3);
}
async function async2(){
console.log(4);
}
async1();
new Promise(function(resolve,reject){
console.log(5);
resolve();
}).then(function(){
console.log(6);
})
console.log(7);
根据js的运行原理解释上面代码的执行顺序:
代码都是从script主程序开始执行;
创建setTimeout宏任务;
执行到async1(),等待await返回值后(此时打印了2,4)继续执行主程序代码,await后的代码被阻断,加入任务队列等待执行;
执行Promise输出5,当前宏任务(script主程序)的程序还没有执行完,创建微任务Promise.then的回调;
执行console.log(7)输出7;
执行任务队列中的微任务,按照顺序,依次打印await后的代码输出3,Promise.then的回调6;
微任务执行完毕,执行宏任务setTimeout,打印1。
// 2 4 5 7 3 6 1
注意async函数里await右边的语句会立即执行,下面的代码进行等待状态,await返回值以后才会继续执行下一条语句。
- 案例说明四
const pro = new Promise((resolve, reject) => {
const innerpro = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 0);
console.log(2);
resolve(3);
});
innerpro.then(res => console.log(res)); // res = 3 加入任务队列
resolve(4);
console.log("pro");
})
pro.then(res => console.log(res)); // res = 4 加入任务队列
console.log("end");
// 2,pro,end,3,4
// 一个promise只能resolve一次,因此最后不会输出1
- 案例五
const pro = new Promise((resolve, reject) => {
console.log('1');
resolve();
}).then(()=>{
console.log('2');
//--- return new Promise(...) 加一个return,打印1-2-3-4-5-6
new Promise((resolve, reject) => {
console.log('3');
resolve();
}).then(()=>{
console.log('4');
}).then(()=>{
console.log('5');
})
}).then(()=>{
console.log('6');
})
// 1 2 3 4 6 5
- 案例六
function main() {
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
// resolve() 如果Promise状态在此处改变--打印1-2-8-4-3-5
setTimeout(() => {
console.log(3);
resolve() //注意resolve的位置
});
}).then(() => {
console.log(4);
});
setTimeout(() => {
console.log(5);
}, 0)
console.log(8);
}
main(); // 1-2-8-3-4-5
过程:函数运行前,宏任务、微任务队列均为空。运行JS代码,执行main(),打印1,运行Promise打印2,此时宏任务setTimeout(3)加入宏任务队列,此时Promise状态还没有改变,没有resolve(),挂起。继续执行setTimeout(5)加入宏任务,继续执行打印8。此时微任务为空,开启新的宏任务,setTimeout(3)打印3,Promise的状态变成resolve(),.then()加入微任务,宏任务执行完执行微任务,打印4,最后执行宏任务setTimeout(5)打印5。
- 案例六
setTimeout(() => {
console.log(1);
},0);
new Promise(function(resolve){
resolve();
console.log(2);
}).then(console.log(3))
console.log(4); // 2--3--4--1
setTimeout(() => {
console.log(1);
},0);
new Promise(function(resolve){
resolve();
console.log(2);
}).then(function() { // 区别在这里!
console.log(3)
})
console.log(4); // 2--4--3--1
- 案例7
console.log('start');
setTimeout(()=>{
console.log('time');
});
new Promise((resolve,reject)=>{
console.log('resolve1');
//resolve('res'); //resolve才会继续then,输出start--resolve1--end--resolve--time
}).then(()=>{
console.log('resolve');
})
console.log('end');
// 输出:start---resolve1---end---time