详解js中的线程,进程, eventLoop

存在下面一段代码:
1
2
3
4
5
6
7
8
9
10
11
setTimeout(function() { console.log(1) }, 0);
new Promise(function(resolve){
console.log(2);
for(var i = 0; i < 10000; i++) {
i === 9999 && resolve()
}
console.log(3);
}).then(function () {
console.log(4);
});
console.log(5);
上面的代码的执行顺序, 依次输出内容分别是什么? 正确的答案是: 2, 3, 5 ,4, 1; 在上面的代码中, 执行的代码顺序如下:
  1. 执行 promise 实例内部的代码, 输出 2;
  2. 顺序执行后面的代码 console.log(3), 输出 3;
  3. 执行 console.log(5), 代码输出 5;
  4. 执行 resolve 函数, 执行 resolve 函数中的代码 console.log(4), 输出 4;
  5. 最后执行 setTimeout 中的代码, 代码执行 console.log(1) , 输出 1; 为什么会按照上面的顺序执行代码, 下面将要进行详细的讲解:

js 中的线程

在 js 中的线程和浏览器中的线程是不同的, 在 js 中是单线程, 在浏览器是多线程的。js 的单线程是指所有的 js 代码都是在 js 引擎上面的一个主线程上面运行的,js 同时只能执行一个任务, 其他的任务则会排队进行等待执行。这些任务被放在一个任务队列中等待执行。在浏览器中, 包括下面这些线程:
  • js 引擎线程(例如 v8 引擎)
  • UI 渲染线程
  • 浏览器事件触发线程
  • 定时触发器线程
  • http 请求线程
这些线程的作用分别是这样的:
  • UI渲染线程用于渲染页面、解析 HTMl CSS, 创建 DOM 树。当页面元素发生重构或者回流的时候, 这个线程就会执行, 重新渲染页面。
  • js引擎用于执行 js 脚本代码,等待任务队列中的任务到来, 并且加以处理
  • 浏览器事件触发线程用于控制用户, 响应交互,当 js 引擎执行代码遇到相关事件的时候, 会将对应的任务添加到事件线程中, 当任务符合触发条件被触发的时候, 触发的任务会被添加到任务队列的队尾, 等待 js 引擎执行完成主线程上面的任务之后执行。
  • 定时触发器线程用于对于 setTimeout 或者 setInterval 进行计数, 因为 js引擎是单线程的, 所以自然计数的任务就不能有 js 引擎来完成, 而是由浏览器单独开出一个定时触发器线程用于计数, 当计数完毕之后, 会将计数完成之后的函数添加到任务队列尾部, 等待 js引擎执行完成主线程上面的任务之后执行。
    这里也就是说有个常见的问题: setTimeout(() => {}, 0);回调是立即执行的吗?并不是, 因为, 需要js 引擎执行完主线程上面的任务之后, 才会执行 任务列表中的任务。
  • http 请求线程, ajax 是委托给浏览器新开一个 http 线程
在上面的这些线程中, js 引擎的线程和UI渲染的线程是互斥的, 因此, 当js执行代码的时候会出现阻塞页面渲染的情况, 这也就是许多前端性能优化中都有提到的将js代码在html代码尾部加载的原因, 同时, 在 js 中操作dom会引发页面的重构或者回流, 这个时候UI渲染线程就会开始工作, 重新渲染页面, js 引擎的主线程就会被挂起,暂停代码执行, 从而影响页面性能, 这也是前端性能优化的一种方式:尽量减少js中直接对于dom的操作。

setTimeout

setTimeout 在 js 中的作用是用来延迟代码执行, 规定代码在延迟多少时间之后执行回调函数代码,在上面关于线程的讲解中, 我们知道浏览器的定时触发器线程会在延迟时间达到之后将回调事件添加到js引擎中的任务队列中, 而在 js 引擎中, 引擎会在执行完成主线程上面的任务之后执行任务队列中的事件, 因此,当代码中存在 setTimeout 的时候, 内部的回调函数会在其他代码执行完毕之后才执行, 尽管我们将延迟时间设为0的情况也是如此:有如下代码:
1
2
setTimeout(function () { console.log(2) }, 0);
console.log(1);
执行结果: 1, 2上面 setTimeout 中的函数会等到 console.log(1) 执行完成之后执行结果。

js 中的事件运行机制

我们知道js是单线程运行的, 那么具体的运行机制是如何的?我们需要知道下面这些概念:
  • js 中分为同步任务和异步任务
  • 同步任务都是在主线程上面执行, 形成一个执行栈
  • 在主线程之外, 事件触发线程管理着一个任务队列, 当异步任务有了运行结果时, 就在任务队列中添加一个事件
  • 当执行栈中的所有的同步任务执行完毕之后, 任务队列中的任务将会添加到执行栈中, 开始执行
事件运行机制的详细图解如下:

js 中的 macrotaskmicrotask

在 js 中, 存在两种任务类型: macrotask(宏任务) 和 microtash (微任务), 这两种任务类型的区别在于执行任务的时机是不同的。
  • macrotask: 宏任务可以理解为执行栈中执行的任务, 在执行任务期间不会中断任务, 浏览器为了能够使 js 内部task与 dom 能够有序的执行, 在执行完成任务之后会进行渲染,
    1
    task ---> 渲染 ---> task
  • microtask微任务会在宏任务执行完毕之后, 进行渲染之前执行
macrotaskmicrotask 中分别包含的几种任务类型:
  • macrotask : 代码块, setTimeout, setInterval
  • microtask: Promise

参考链接

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理JavaScript 运行机制详解:再谈Event Loop