JS运行机制

126 阅读8分钟

JS运行机制

开局一问

下列代码输出顺序是否能答对呢?能答对是否是知道为什么呢?不知道的话跟着我往下看。

function test() {
  console.log(1)
  setTimeout(function () { 	// timer1
    console.log(2)
  }, 1000)
}

test();

setTimeout(function () { 		// timer2
  console.log(3)
})

new Promise(function (resolve) {
  console.log(4)
  setTimeout(function () { 	// timer3
    console.log(5)
  }, 100)
  resolve()
}).then(function () {
  setTimeout(function () { 	// timer4
    console.log(6)
  }, 0)
  console.log(7)
})

console.log(8)

// 输出1,4,8,7,3,6,5,2

进程和线程

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

1.1 什么是进程?

官网说法:进程是CPU资源分配的最小单位。

字面意思就是进行中的程序,可以理解为一个独立运行且用于偶自己的资源空间的任务程序,进程包括运行中的程序和程序所用到的内存子资源。

CPU可以拥有很多进程,打开一个软件就有一个或多个进程,电脑运行很多个软件就会卡,是因为CPU需要给每个进程分配内存资源空间,内存有限,进程越多就越卡。

例如:浏览器打开一个tab相当于一个进程

1.2 什么是线程?

线程是CPU调度的最小单位

线程是建立在进程基础上的一次程序运行单位,一个进程可以有多个线程。

一个进程由一个或多个线程组成,线程可以理解为是一个进程中代码的不同执行路线。

一个进程中只有一个执行流称作为单线程,即程序执行时,路线是按顺序排好的,前面的处理完,才能执行后面的。

一个进程中有多个执行流称作为多线程,即程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

调度和切换:线程上下文切换比进程上下文切换要快得多。

1.3 浏览器

1.3.1 浏览器包含哪些进程?

  1. Browser进程
    1. 浏览器的主进程——负责协调、主控
    2. 负责浏览器页面显示
    3. 负责各个页面的管理
    4. 网络资源的管理、下载等
  2. 第三方插件进程
    1. 每种类型的插件对应一个进程,当使用插件的时候创建
  3. GPU进程 —— 用于3D绘制等
  4. renderer渲染进程
    1. 通常说的浏览器内核
    2. 每个Tab页面都有一个渲染进程
    3. 主要作用为页面渲染、脚本执行、事件处理等

假设浏览器是单进程,那么某一个Tab或者第三方插件崩溃了,就影响整个浏览器。

1.3.2 渲染进程

页面的渲染,JS的执行,事件的循环,都在渲染进程内执行,所以我们要重点了解渲染进程

渲染进程是多线程的,看渲染进程的一些常用较为主要的线程:

GUI渲染线程
  • 负责渲染浏览器页面,解析HTML、CSS,构建DOM树和RenderObject树,布局和绘制等
  • 修改了一些元素的颜色或者背景色,页面就会重绘
  • 修改了元素的尺寸,页面就会回流
  • 当页面需要重绘和回流的时候,GUI线程执行绘制页面
  • 回流的成本比重绘的成本高,尽量避免这两种情况
  • GUI渲染线程和JS引擎线程是互斥的
    • JS引擎执行的时候GUI会被挂起
    • GUI更新保存在一个队列中,等到JS引擎空闲的时候立即执行
JS引擎线程
  • JS引擎就是JS内核,负责解析和运行JS脚本程序
  • 一直等待这任务队列中任务的到来,然后加以处理
    • 浏览器同时只能有一个JS引擎在运行JS程序,所以Js是单线程运行的
    • 一个Tab页中无论什么时候只有一个JS线程运行JS程序
  • JS引擎会堵塞GUI渲染线程
    • JS运行时间过长,造成页面渲染不连贯,导致页面渲染加载堵塞
    • GUI解析HTML的时候,遇到了script标签,就会停止GUI 的渲染,先执行js代码,执行完后,GUI继续渲染;代码执行时间过长会导致页面卡顿,因此才有了(defer 和 async
事件触发线程
  • 属于浏览器,而不是jS引擎,用来控制事件循环,并且管理着一个事件队列
  • 当js执行遇到事件绑定和异步操作,会走事件触发线程将对应的时间添加到对应的线程中,等异步事件有了结果,将他们的回调添加到事件队列中,等待js引擎空闲时来处理
  • 当对应的事件触发时,该线程会把时间添加到带处理队列队尾,等待js执行
  • 因为JS是单线程,所以这些待处理队列中的事件都得排队等待
定时器的线程
  • setInterval与setTimeout所在线程
  • 浏览器定时计数器不是由JS引擎计数的(因为堵塞就会影响计时的准确性)
  • 通过单线程来计时并触发定时,计时结束后将回调事件放入到事件触发线程的时间队列中
  • W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
异步的HTTP请求的线程
  • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
  • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中

事件循环(Event Loop)基础

JS分为同步任务和异步任务。同步任务都在主线程(这里的主线程就是JS引擎线程)上执行,会形成一个执行栈。

事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放一个事件回调。一旦执行栈中的所有同步任务执行完毕(也就是JS引擎线程空闲了),系统就会读取任务队列,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中。

反复执行,就是我们所谓的事件循环。

image.png

宏任务& 微任务

由于JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染

宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成在当前宏任务执行后立即执行的任务。

1. 宏任务

可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他。

常见的宏任务(一般记住这几个宏任务,其他基本上都是微任务):

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

2. 微任务

微任务可以理解成在当前宏任务执行后立即执行的任务。

常见微任务

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

注意:new Promise(() => {}).then() 中,前面的 new Promise() 这一部分是一个构造函数,这是一个同步任务,后面的 .then() 才是一个异步微任务

async/await

在使用await关键字与Promise.then效果类似,await 以前的代码,相当于与 new Promise 的同步代码,await 以后的代码相当于 Promise.then的异步

setTimeout(() => console.log(4))

async function test() {
  console.log(1)
  await Promise.resolve() // 这一行以及同作用域下后面代码 类似于 then的回调,属于微任务
  console.log(3)
}

test()

console.log(2)
// 输出1 2 3 4

3. 事件循环进阶版

image-20230219155000129.png

开局问题解析

通过这一系列的概念解析,开局问题应该大致有了思路吧,下面看看是不是和你想的一样吧。

因为JS是从上到下执行的

  1. 执行同步代码test(),遇到console.log(1), 因此打印 1
  2. 遇到setTimeout定时为1000ms,因此1000ms后将回调放入事件触发器的任务队列(宏任务)
  3. 执行完test(),再次遇到setTimeout,这个没有写延迟时间,因此为默认的0,将回调放入宏任务队列
  4. 接着遇到了Promise,因为Promise中代码为同步代码,因此执行console.log(4),打印4;执行setTimeout延迟为100ms,因此100ms后将回调放入宏任务队列
  5. Promise.then是微任务,因此将then的回调放入微任务队列中
  6. then之后,遇到同步代码console.log(8),直接打印8;至此同步任务执行完毕;开始执行微任务
  7. 微任务队列中只有一个Promise.then的回调函数,执行回调,遇到了setTimeout延迟为0,将回调放入宏任务队列;然后执行同步代码console.log(7),因此直接打印7;微任务队列执行完毕
  8. 开始执行,宏任务队列,根据setTimeout的延迟时间可得到宏任务队列中的回调函数顺序为console.log(3)、console.log(6)、console.log(5)、console.log(2);所以按顺序打印为 3,6,5,2