JS运行机制 | JS

JS运行机制

深入剖析底层原理 🤔 本篇笔记涉及进程和线程,JS单线程机制,浏览器机制,宏任务微任务,async/await等内容

进程和线程

  • 什么是进程
    • 进程CPU资源分配的最小单位
    • 进程包括运行中的程序和程序所使用到的内存和系统资源
  • 什么是线程
    • 线程CPU调度的最小单位
    • 线程是建立在进程基础上的一次程序运行单位 程序中的一个执行流
      • 单线程:一个进程中只有一个执行流
      • 多线程:一个进程中有多个执行流
  • 进程和线程的区别
    • 进程是操作系统分配资源的最小单位,线程是程序执行的最小单位
    • 一个进程由一个或多个线程组成,线程可以理解为是一个进程中代码的不同执行路线
    • 进程之间相互独立,但同一进程下的各个线程间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)
    • 调度和切换:线程上下文切换比进程上下文切换要快得多
  • 多进程和多线程
    • 多进程:多进程指的是在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态
    • 多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务

JS的单线程机制

  • 脚本语言 与用户互动 操作DOM
  • 不是单线程会有同步问题

浏览器

浏览器是多进程的

浏览器包含的进程

  • Browser进程
    • 浏览器的主进程(负责协调、主控),该进程只有一个
    • 负责浏览器界面显示,与用户交互。如前进,后退等
    • 负责各个页面的管理,创建和销毁其他进程
    • 将渲染(Renderer)进程得到的内存中的Bitmap(位图),绘制到用户界面上
    • 网络资源的管理,下载等
  • 第三方插件进程
    • 每种类型的插件对应一个进程,当使用该插件时才创建
  • GPU进程
    • 该进程也只有一个,用于3D绘制等等
  • 渲染进程(重)
    • 即通常所说的浏览器内核(Renderer进程,内部是多线程)
    • 每个Tab页面都有一个渲染进程,互不影响
    • 主要作用为页面渲染,脚本执行,事件处理

渲染进程Renderer

GUI渲染线程
  • 负责渲染浏览器界面,解析HTML,CSS构建DOM树和RenderObject树布局和绘制
    • 解析html代码(HTML代码本质是字符串)转化为浏览器认识的节点,生成DOM树,也就是DOM Tree
    • 解析css,生成CSSOM(CSS规则树)
    • 把DOM Tree 和CSSOM结合,生成Rendering Tree(渲染树)
  • 当我们修改了一些元素的颜色或者背景色,页面就会重绘(Repaint)
  • 当我们修改元素的尺寸,页面就会回流(Reflow)
  • 当页面需要Repaing和Reflow时GUI线程执行,绘制页面
  • 回流(Reflow)比重绘(Repaint)的成本要高,我们要尽量避免Reflow和Repaint
  • GUI渲染线程与JS引擎线程是互斥的
    • 当JS引擎执行时GUI线程会被挂起(相当于被冻结了)
    • GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
JS引擎线程
  • JS引擎线程就是JS内核,负责处理Javascript脚本程序(例如V8引擎)
  • JS引擎线程负责解析Javascript脚本,运行代码
  • JS引擎一直等待着任务队列中任务的到来,然后加以处理
    • 浏览器同时只能有一个JS引擎线程在运行JS程序,所以js是单线程运行的
    • 一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
  • GUI渲染线程与JS引擎线程是互斥的,js引擎线程会阻塞GUI渲染线程
    • 就是我们常遇到的JS执行时间过长,造成页面的渲染不连贯,导致页面渲染加载阻塞(就是加载慢)
    • 例如浏览器渲染的时候遇到<script>标签,就会停止GUI的渲染,然后js引擎线程开始工作,执行里面的js代码,等js执行完毕,js引擎线程停止工作,GUI继续渲染下面的内容。所以如果js执行时间太长就会造成页面卡顿的情况
事件触发线程
  • 属于浏览器而不是JS引擎,用来控制事件循环,并且管理着一个事件队列(task queue)
  • 当js执行碰到事件绑定和一些异步操作(如setTimeOut,也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会走事件触发线程将对应的事件添加到对应的线程中(比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待js引擎线程空闲时来处理。
  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
  • 因为JS是单线程,所以这些待处理队列中的事件都得排队等待JS引擎处理
定时触发器线程
  • setIntervalsetTimeout所在线程
  • 浏览器定时计数器并不是由JavaScript引擎计数的(因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确)
  • 通过单独线程来计时并触发定时(计时完毕后,添加到事件触发线程的事件队列中,等待JS引擎空闲后执行),这个线程就是定时触发器线程,也叫定时器线程
  • W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
异步http请求线程
  • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
  • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中再由JavaScript引擎执行
  • 简单说就是当执行到一个http异步请求时,就把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),再把回调函数添加到事件队列,等待js引擎线程来执行

事件循环Event Loop

  • js分为同步任务异步任务
  • 同步任务都在主线程 ,形成一个执行栈
  • 事件触发线程管理着一个任务队列, 异步任务有结果之后放一个事件回调
  • 执行栈执行完毕后,读取任务队列添加到执行栈中开始执行

JS引擎线程只会执行执行栈中的事件,执行栈中的代码执行完毕,就会读取事件队列中的事件并添加到执行栈中继续执行,这样反反复复就是我们所谓的事件循环(Event Loop)

总之就是 js线程控制执行着执行栈中的任务

事件触发线程控制着任务队列, js有空的时候才执行任务队列里面的

定时器触发线程是单独一个线程用来计时,到时间之后添加到任务队列里面等待

异步http请求线程是单独一个线程请求资源,请求成功之后添加到任务队列里面等待

当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

事件循环的基本流程如下:

  • 执行同步代码,将Promise对象的executor函数立即执行。
  • 如果executor中包含异步操作(比如setTimeout),它们会被放入宏任务队列。
  • 当执行栈为空时,JavaScript会检查微任务队列,执行微任务。微任务包括Promise的回调。then
  • 接着,JavaScript会检查宏任务队列,执行宏任务。宏任务包括setTimeout等。

宏任务&微任务

宏任务

macrotask (task)

1
宏任务 -> GUI渲染 -> 宏任务 -> GUI ...
  • 常见的宏任务
    • 主代码块
    • setTimeout
    • setInterval
    • setImmediate ()-Node
    • requestAnimationFrame ()-浏览器

微任务

microtask(jobs)

1
宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...
  • 常见微任务
    • process.nextTick ()-Node
    • Promise.then()
    • catch
    • finally
    • Object.observe
    • MutationObserver

简单区分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:pink';

//页面直接变成pink 因为这是一次宏任务 都结束后才渲染
document.body.style = 'background:blue';
setTimeout(()=>{
document.body.style = 'background:black'
},200)

//先blue再马上black 属于两次宏任务 之间有渲染
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:pink'
});
console.log(3);

//输出 1 3 2 then是异步执行
//直接变成pink因为宏任务执行后执行Promise.then微任务 之后再渲染

首先执行一个宏任务,执行结束后判断是否存在微任务

有微任务先执行所有的微任务,再渲染,没有微任务则直接渲染

然后再接着执行下一个宏任务

  • 执行顺序
    • 执行当前宏任务的同步代码
    • 执行当前宏任务中的所有微任务
    • 执行下一个宏任务

关于Promise

1
2
3
4
5
6
7
8
9
10
11
new Promise((resolve) => {
console.log(1)
resolve()
}).then(()=>{
console.log(2)
})
console.log(3)

//输出 1 3 2
//new Promise()是一个构造函数 同步任务
//.then()是一个异步微任务

定时器情况

关于async/await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
setTimeout(() => console.log(4))

async function test() {
console.log(1)
await Promise.resolve()
console.log(3)
}

test()

console.log(2)

//输出 1 2 3 4
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

某道学长的面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async function A(){
console.log(111);
await B();
console.log(222); //这里可能需要注意一下,是放入微任务队列
}

async function B(){
console.log(333);
}

console.log(444);
A();
setTimeout(()=>{
console.log(555);
},0);
new Promise((resolve,reject)=>{
console.log(666);
resolve(777);
}).then((reason)=>{
console.log(reason);
})
console.log(888);

//444 111 333 666 888 222 777 555
  • await关键字遇到一个Promise时,它会将后续的代码(在这种情况下是打印222)放入微任务队列,以便在Promise解析为已完成状态后执行。
  • 微任务队列是一个异步任务队列,其中的任务会在主事件循环之前执行。这确保了await后面的代码可以尽快执行,而不会被其他任务阻塞。