事件循环机制(Event Loop)

问题:setTimeOut(() => {}, 0)是立即执行吗?下面代码输出结果是什么?

setTimeout(() => {
    
    
  console.log('setTimeout');
}, 0);
//Promise.resolve().then(() => {
    
    
  //console.log('Promise');
//});
console.log('start');
const date = new Date().getTime();
for (let i = 1; i < 100000000; i++) {
    
    }
console.log('end', new Date().getTime() - date);

一、Event Loop 是什么

In computer science, the event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program. The event loop works by making a request to some internal or external “event provider” (that generally blocks the request until an event has arrived), then calls the relevant event handler (“dispatches the event”). The event loop is also sometimes referred to as the message dispatcher, message loop, message pump, or run loop.

上面是维基百科对Event Loop的解释,简单来说就是Event Loop是一个程序结构,用于等待和分派消息和事件,是一种解决单线程不会阻塞的机制。

二、进程和线程

正式介绍Event Loop前,先简单了解下进程和线程

定义:

  1. 进程:进程是 CPU 资源分配的最小单位
  2. 线程:线程是 CPU 调度的最小单位

简单来说,进程简单理解就是我们平常使用的程序,如 QQ,浏览器,网盘等。进程拥有自己独立的内存空间地址,拥有一个或多个线程,而线程就是对进程粒度的进一步划分。
更通俗的来说,进程就像是一家工厂,多个工厂之间是独立存在的。而线程就像是工厂中的那些工人,共享资源,完成同一个大目标。

js的单线程

JavaScript 是一门动态的解释型的语言,具有跨平台性。
JavaScript 从诞生起就是单线程,原因大概是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。
JavaScript 的单线程是指 JavaScript 引擎是单线程的,JavaScript 的引擎并不是独立运行的,跨平台意味着 JavaScript 依赖其运行的宿主环境 — 浏览器(大部分情况下是浏览器)。
浏览器需要渲染 DOM,JavaScript 可以修改 DOM 结构,JavaScript 执行时,浏览器 DOM 渲染停止。如果 JavaScript 引擎线程不是单线程的,那么可以同时执行多段 JavaScript,如果这多段 JavaScript 都操作 DOM,那么就会出现 DOM 冲突。
举个例子来说,在同一时刻执行两个 script 对同一个 DOM 元素进行操作,一个修改 DOM,一个删除 DOM,那这样话浏览器就会懵逼了,它就不知道到底该听谁的,会有资源竞争,这也是 JavaScript 单线程的原因之一。

三、浏览器

浏览器的多线程

JavaScript 运行的宿主环境浏览器是多线程的。
以 Chrome 来说,可以通过 Chrome 的任务管理器来看看。
[图片]

当你打开一个 Tab 页面的时候,就创建了一个进程。如果从一个页面打开了另一个页面,打开的页面和当前的页面属于同一站点的话,那么这个页面会复用父页面的渲染进程。
一个Tab 页包含5种进程:

  • 浏览器主进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程:核心任务是将HTML、CSS、JavaScript转化为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,chrome默认为每一个标签页开启一个渲染进程。
  • GPU进程:用于支持css的3d效果、图形、视频渲染
  • 网络进程:主要负责页面的网络资源加载。
  • 插件进程:负责浏览器插件运行

浏览器渲染进程常驻线程

  1. GUI 渲染线程
  • 绘制页面,解析 HTML、CSS,构建 DOM 树,布局和绘制等
  • 页面重绘和回流
  • 与 JS 引擎线程互斥,也就是所谓的 JS 执行阻塞页面更新
  1. JS 引擎线程(主线程)
  • 负责 JS 脚本代码的执行
  • 负责准执行准备好待执行的事件,即定时器计数结束,或异步请求成功并正确返回的事件
  • 与 GUI 渲染线程互斥,js任务执行时间过长将阻塞页面的渲染
  1. 事件触发线程
  • 负责将准备好的事件交给 JS 引擎线程执行
  • 多个事件加入任务队列的时候需要排队等待(JS 的单线程),比如鼠标事件时,会将这些任务添加到事件触发线程中,等事件触发时,会将任务从事件触发线程中取出,放到消息队列的队尾等待执行
  1. 定时器触发线程
  • 负责执行异步的定时器类的事件,如 setTimeout、setInterval
  • 定时器到时间之后把注册的回调加到任务队列的队尾
  1. HTTP 请求线程
  • 负责执行异步请求
  • 主线程执行代码遇到异步请求的时候会把函数交给该线程处理,当监听到状态变更事件,如果有回调函数,该线程会把回调函数加入到任务队列的队尾等待执行

四、浏览器端的 Event Loop

什么是同步与异步?

上文已经说过了 JavaScript 是一门单线程的语言,一次只能执行一个任务,如果所有的任务都是同步任务,那么程序可能因为等待会出现假死状态,这对于一个用户体验很强的语言来说是非常不友好的。
比如说向服务端请求资源,你不可能一直不停的循环判断有没有拿到数据。因此,在 JavaScript 中任务有了同步任务和异步任务,异步任务通过注册回调函数,等到数据来了就通知主程序。
简单的介绍一下同步任务和异步任务的概念。

  1. 同步任务:必须等到结果来了之后才能做其他的事情,举例来说就是你烧水的时候一直等在水壶旁边等水烧开,期间不做其他的任何事情。
  2. 异步任务:不需要等到结果来了才能继续往下走,等结果期间可以做其他的事情,结果来了会收到通知。举例来说就是你烧水的时候可以去做自己想做的事情,听到水烧开的声音之后再去处理。

为什么单线程却可以异步?

JavaScript 的确是一门单线程语言,但是浏览器 UI 是多线程的,异步任务借助浏览器的线程和 JavaScript 的执行机制实现。例如,setTimeout 就借助浏览器定时器触发线程的计时功能来实现。
在这里插入图片描述

上图是一张 JS 的运行机制图,Js 运行时大致会分为几个部分:

  1. Call Stack:调用栈(执行栈),所有同步任务在主线程上执行,形成一个执行栈,因为 JS 单线程的原因,所以调用栈中每次只能执行一个任务,当遇到的同步任务执行完之后,由任务队列提供任务给调用栈执行。
  2. Task Queue:任务队列,存放着异步任务,当异步任务可以执行的时候,任务队列会通知主线程,然后该任务会进入主线程执行。任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中。
    根据上述描述,Event Loop 也可以理解为:不断地从任务队列中取出任务执行的一个过程。

Event Loop

Event Loop 很好的调度了任务的运行,现在来看看它的调度运行机制。
JavaScript 的代码执行时,主线程会从上到下,一步步的执行代码,同步任务会被依次加入执行栈中先执行,异步任务,放到Web API中,等待时机会,在拿到结果的时候将注册的回调函数放入任务队列(Event Queue),当执行栈中的没有任务在执行的时候,引擎会从任务队列中读取任务压入执行栈(Call Stack)中处理执行。
举个栗子

  • setTimeout(cb, 1000),当1000ms后,就将cb压入Event Queue。
  • ajax(请求条件, cb),当http请求发送成功后,cb压入Event Queue0。
setTimeout(() => {
    
    
  console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
    
    
  console.log('Promise');
});
console.log('start');
const date = new Date().getTime();
for (let i = 1; i < 100000000; i++) {
    
    }
console.log('end', new Date().getTime() - date);
宏任务(Macro-task)和微任务(Micro-task)

异步任务又分为宏任务和微任务,JS 运行时任务队列会分为宏任务队列(Task queue)和微任务队列(Micro task queue),分别对应宏任务和微任务。
介绍一下(浏览器环境的)宏任务和微任务大致有哪些:

  1. 宏任务:
  • script(可以理解为外层同步代码,作为入口 )
  • setTimeout、setInterval:通过 JavaScript 宿主环境提供的定时器函数,可以设置一定的时间后产生宏任务执行对应的回调函数。
  • I/O 操作: 用户在页面上进行交互操作(例如点击、滚动、输入等)
  • UI 渲染 :当 DOM 元素发生变化时(例如节点的添加、删除、属性的修改等),会产生宏任务来更新页面。
  1. 微任务
  • Promise.then
  • MutationObserver
    事件运行顺序
  1. 执行同步任务,同步任务不需要做特殊处理,直接执行(下面的步骤中遇到同步任务都是一样处理) — 第一轮从 script开始
  2. 从宏任务队列中取出队头任务执行
  3. 如果产生了宏任务,将宏任务放入宏任务队列,下次轮循的时候执行
  4. 如果产生了微任务,将微任务放入微任务队列
  5. 执行完当前宏任务之后,取出微任务队列中的所有任务依次执行
  6. 如果微任务执行过程中产生了新的微任务,则继续执行微任务,直到微任务的队列为空
  7. 轮循,循环以上 2 - 6
    总的来说就是:同步任务/宏任务 -> 执行产生的所有微任务(包括微任务产生的微任务) -> 同步任务/宏任务 -> 执行产生的所有微任务(包括微任务产生的微任务) -> 循环…

小试牛刀:

// 宏任务、微任务 执行顺序面试题
console.log(‘1’);

setTimeout(function () {
  console.log('2');
 
  new Promise(function (resolve) {
    console.log('4');
    resolve();
  }).then(function () {
    console.log('5')
  })
})

new Promise(function (resolve) {
  console.log('7');
  resolve();
}).then(function () {
  console.log('8')
})

setTimeout(function () {
  console.log('9');

  new Promise(function (resolve) {
    console.log('11');
    resolve();
  }).then(function () {
    console.log('12')
  })
})

答案 :
第一轮 执行外面同步代码 : 1 7
第二轮 执行 微任务 : 8
第三轮 宏任务 第一个setTimeout : 同步 2 4 微任务 5 第二个setTimeout:同步 9 11 微任务 12
整体答案: 1、7 、、8、2、4、、5、9、11、12

猜你喜欢

转载自blog.csdn.net/qq_45234274/article/details/130867400