浏览器的多进程架构、事件循环运行机制解析

549 阅读10分钟

关于浏览器事件循环的基础知识

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

    • 称为浏览器渲染进程或浏览器内核,内部是多线程的。主要负责页面渲染,脚本执行,事件处理等。(本文重点分析)

浏览器内核

浏览器是多进程的,浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。浏览器内核有多种线程在工作:

  1. GUI渲染线程:
    • 负责渲染页面,解析HTML,CSS构成DOM树等,当页面重绘或回流时调用该线程。
    • 和JS引擎线程互斥,当JS引擎线程在工作的时候,GUI渲染线程会被挂起,GUI更新被放入在JS任务队列中,等待JS引擎线程空闲的时候继续执行。
  2. JS引擎线程:
    • 单线程工作,负责解析运行javascript脚本。
    • 和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
  3. 事件触发线程:
    • 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
  4. 定时器触发线程:
    • 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
    • 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待JS引擎处理。
  5. http请求线程:
    • http 请求的时候会开启一条请求线程。
    • 请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。

JS事件循环

  • 为什么?
    • JavaScript 引擎是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。如果某个执行时间太长,就容易造成阻塞;为了解决这一问题,JavaScript引入了事件循环机制。
  • 是什么?
    • JavaScript 单线程中的任务分为同步任务和异步任务。从下面流程图中可以看到,主线程不断从任务队列中读取事件,这个过程是循环不断的,这种运行机制就叫做Event Loop(事件循环)! 同步任务与异步任务流程图

JS事件循环机制

JavaScript事件循环机制分为浏览器和Node事件循环机制,这里主要讲的是浏览器部分。 Javascript 有一个main thread主线程和call-stack调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。这里先了解JS执行环境及作用域,可以看这篇:浅谈JS执行环境和作用域

  1. JS调用栈
    • JS 调用栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。
  2. 同步任务、异步任务
    • JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
  3. 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,输出34,当前宏任务(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后的代码输出3Promise.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

参考文档: 参考文档1参考文档2参考文档3