原理学习 | 浏览器环境 js引擎运行机制

496 阅读6分钟

为什么页面会闪烁?异步任务是如何在单线程的js中运行的?这一切都跟浏览器的运行机制有关系。为了更好的开发页面,我初步梳理了浏览器的运行机制,请大家一起学习,批评指正。

进程与线程

CPU是计算机的核心,承担所有的计算任务。

进程CPU的最小资源分配单位,进程包括运行中的程序和程序所使用到的内存和系统资源。

线程CPU调度的最小单位,线程就是程序中的一个执行流,一个进程可以有多个线程。一个进程下只有一个线程,称之为单线程,一个进程下有多个线程,称之为多线程

单线程与多线程

进程之间相互独立,同一进程下的各个线程共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)。

也就是说,同一个进程下,多个线程是共享的数据,线程A、线程B可操作同一个对象。这时候对于浏览器页面DOM来说,如果两个线程同时操作一个DOM节点,一个要删除,一个要添加,那么就不知道该以哪个为准。于是浏览器在与DOM相关的页面脚本执行与UI渲染方面,选择了单线程的执行方式。

浏览器的运行机制

浏览器是多进程的,例如Chrome,我们每打开一个Tab页就会产生一个进程,我们打开很多标签页不关,电脑会越来越卡,很耗CPU。浏览器包含这些类型的进程:

  • 浏览器的主进程(负责协调、主控),该进程只有一个。
  • 负责浏览器界面显示,与用户交互。如前进,后退。
  • 负责各个页面的管理,创建和销毁其他进程。
  • 网络资源的管理,如下载。
  • 第三方插件进程,每种类型的插件对应一个进程,当使用该插件时才创建。
  • GPU进程,该进程也只有一个,用于3D绘制。
  • 页面渲染进程,即通常所说的浏览器内核
    • 每开一个Tab页面都会产生一个渲染进程,进程间互相独立。
    • 主要作用为页面渲染脚本执行事件处理等。
      • 进程内部是多线程,包括GUI渲染线程js引擎线程事件处理线程定时器线程异步http请求线程等。
      • 其中,事件处理线程定时器线程异步http请求线程是可以和其他线程并发执行的,并且,这些类型的线程每种都可以有多个。
      • GUI渲染线程js引擎线程是两个单线程类型线程,他们在一个进程内,都只能存在一个。并且他们是互斥的,只能一个线程执行完一个tick之后,才会执行另一个线程。这是因为他们都与DOM相关,而DOM的变化必须只能以一个线程为准。所以有的时候,js执行过慢会让页面渲染卡顿。
      • js引擎线程是一个单线程(主线程是单线程,可以开启多个子线程如:WebWorker,子线程完全受主线程控制且不可操作DOM),就是JS内核,负责处理Javascript脚本程序(例如V8引擎)。他拥有计算能力,可以执行js脚本的计算逻辑,还可以进行DOM操作,还可以调用或者绑定页面进程的其他线程接口的异步服务,例如异步http请求计时器浏览器事件

js引擎线程

js引擎线程,他拥有一个执行栈,以及一个宏任务队列和一个微任务队列。

  • 首先由执行栈执行脚本本身的同步代码,遇到异步操作就调用异步任务相关的线程(例如定时器、http请求等)去执行并等待有回调结果放入任务队列,或者直接放入任务队列
  • 这些异步操作,有些会产生一个微任务的回调,有些会产生一个宏任务的回调。所以任务队列有两个,一个宏任务队列,一个微任务队列
  • 执行栈所有同步代码都执行完毕,就会先查看微任务队列,清空微任务队列后,一个tick(Event Loop - 事件循环)完成。
  • 每当一个tick完成,就会让位于GUI渲染线程,如果有等待渲染的任务,就进行一次UI渲染,没有就从宏任务队列轮询宏任务,继续执行下一次宏任务。

Event Loop

以下是完整的Event Loop运行机制

宏任务与微任务

宏任务

在ECMAScript中,宏任务被称为macrotask也被称为task

常见的宏任务

  1. 主代码块
  2. setTimeout
  3. setInterval
  4. setImmediate (Node)
  5. requestAnimationFrame (浏览器)

微任务

在ECMAScript中,微任务被称为microtask也被称为jobs

ES6新引入了Promise标准,同时浏览器实现上多了一个microtask微任务概念。我们已经知道宏任务结束后,会执行UI渲染,然后执行下一个宏任务,而微任务可以理解成在“当前宏任务执行后立即执行的任务”。当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完。

常见的微任务

  1. process.nextTick (Node)
  2. Promise.then
  3. catch
  4. finally
  5. Object.observe
  6. MutationObserver

Promise

new Promise()本身是一个构造函数,里面的匿名函数是同步任务,将会同步执行。而后面的thencatch是异步的微任务

new Promise(resolve => {
    console.log(1)
    resolve()
}).then(()=>{
    console.log(3)
})
console.log(2)

// 输出结果:1,2,3

async/await

async/await本质上基于Promise和Generator的语法糖,而Promise是属于微任务的一种。所以await关键字与Promise.then效果类似,他相当于把执行await的当行代码,包装成一个Promise对象去执行,所以await当行代码会立即执行。而await后面的代码块,会被打包放在一个then的回调函数里。所以后面的代码块变成了异步微任务,不在立即执行,而是被执行栈放到微任务队列后,就继续执行函数外面的同步代码。

// 工具函数
async function print (i) {
    console.log(i);
}

// 测试函数
async function test() {
    console.log(2);
    await print(3);
    console.log(6);
}

// 开始执行
console.log(1);


test();
new Promise(resolve => {
    console.log(4);
    resolve();
}).then(() => {
    console.log(7);
})

console.log(5);

// 输出结果:1,2,3,4,5,6,7

node环境的js引擎运行机制

虽然NodeJS中的JavaScript运行环境也是V8,也是单线程,但是,还是有一些与浏览器环境中的表现是不一样的。区别就是nodejs的宏任务分好几种类型,而这好几种又有不同的任务队列,而不同的任务队列又有顺序区别,而微任务是穿插在每一种宏任务之间的。所以node的任务机制更为复杂。

浏览器只有一个宏任务队列和一个微任务队列,相对简单清晰。

参考

[1]「硬核JS」一次搞懂JS运行机制

[2]多进程浏览器、多线程页面渲染与js的单线程

[3]8张图让你一步步看清 async/await 和 promise 的执行顺序

[4]promise、async/await在任务队列中的执行顺序

[5]并发模型与事件循环