js事件循环机制EventLoop

js事件循环机制EventLoop

    • 什么是进程?
      • 单进程和多进程
    • 什么是线程?
      • 单线程和多线程
    • 浏览器的进程分类
    • 事件循环(Event Loop)
      • 单线程处理安排好的任务
      • 在线程运行中处理新任务
      • 处理其他线程发送过来的任务
        • 消息队列
        • 消息队列+循环
      • 处理其他进程发送过来的任务
      • 消息队列中的任务类型
        • macro-task(宏任务)
        • micro-task(微任务)
        • requestAnimationFrame(RAF)
        • requestIdleCallback
        • 事件循环,宏任务,微任务的关系
        • requestAnimationFrame、requestIdleCallback、事件循环、宏任务,微任务的关系
        • 举个
        • 小练习
    • 公众号

什么是进程?

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

单进程和多进程

顾名思义就是只有一个进程就是单线程,有超过1个进程就是多进程

什么是线程?

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程是不能单独存在的,它是由进程来启动和管理的

单线程和多线程

一个进程可以包含一个线程的时候就是单线程,包含几个线程的时候就是多线程。

浏览器的进程分类

  • 1 个浏览器(Browser)主进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 1 个GPU 进程:Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
  • 1 个网络(NetWork)进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 多个渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • 多个插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

事件循环(Event Loop)

每个渲染进程都有一个主线程,并且主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要消息队列和事件循环系统来统筹调度这些任务。

单线程处理安排好的任务

我们知道JS是单线程的,一般的代码都是按顺序执行的,也就是已知的安排好的任务都是按顺序执行的,等这个任务执行完成之后就会推出线程。

js事件循环机制EventLoop_第1张图片

一般情况肯定没有我们想的那么好,全都是已知的任务,有可能中间插入其他任务需要执行,这个时候应该怎么办呢?

在线程运行中处理新任务

我们可以加一个循环,等待事件进入,然后再执行

js事件循环机制EventLoop_第2张图片

处理其他线程发送过来的任务

刚刚一直都在讨论主线程上的任务执行,那如果有其他线程的任务发给主线程,这个时候主线程怎么处理呢?

js事件循环机制EventLoop_第3张图片

消息队列

消息队列是一种数据结构,可以存放要执行的任务。数据结构里的队列就是“先进先出”的特性,一般添加任务就是添加到队列尾部,然后从队列的头部取出任务来操作。

js事件循环机制EventLoop_第4张图片

消息队列+循环

添加了一个消息队列后,其他的线程发送过来的事件就添加到消息队列的尾部,然后主线程会循环的从消息队列的头部取出任务再执行任务。

js事件循环机制EventLoop_第5张图片

处理其他进程发送过来的任务

刚刚我们是处理其他线程发送给主线程的任务,现在是处理其他进程发送过来的任务,一字之差,其实步骤差不多的。整个渲染进程会有一个IO线程来接收其他进程发送过来的任务,然后再把接收到的其他进程任务添加到消息队列的尾部,渲染主线程就循环的取出任务,执行任务。

js事件循环机制EventLoop_第6张图片

消息队列中的任务类型

macro-task(宏任务)

  • 包括整体代码script,setTimeout,setInterval

micro-task(微任务)

  • Promise的resolve和reject会创建微任务,MutationObserver,如果监听了某个节点,那么通过DOMAPI修改这些被监听的节点也会产生微任务。await 的下一行开始,process.nextTick(类似node.js版的"setTimeout")

requestAnimationFrame(RAF)

  • window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
  • 它是由系统来决定回调函数的执行时机的,会请求浏览器在下一次重新渲染之前执行回调函数。无论设备的刷新率是多少,requestAnimationFrame 的时间间隔都会紧跟屏幕刷新一次所需要的时间;例如某一设备的刷新率是 75 Hz,那这时的时间间隔就是 13.3 ms(1 秒 / 75 次)。需要注意的是这个方法虽然能够保证回调函数在每一帧内只渲染一次,但是如果这一帧有太多任务执行,还是会造成卡顿的;因此它只能保证重新渲染的时间间隔最短是屏幕的刷新时间。

具体还是可以看MDN:requestAnimationFrame

requestIdleCallback

  • window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。
  • 你可以在空闲回调函数中调用requestIdleCallback(),以便在下一次通过事件循环之前调度另一个回调。
  • 和requestAnimationFrame 每一帧必定会执行不同,requestIdleCallback 是捡浏览器空闲来执行任务。

具体还是可以看MDN:requestIdleCallback

事件循环,宏任务,微任务的关系

1、整体的代码属于一个大的宏任务
2、遇到promise.then等微任务的时候会放进微任务队列尾部
3、遇到setTimeout,setInterval 等宏任务的时候会放入宏任务的队列尾部
4、最大的宏任务执行完后,查看有没有微任务,有的话执行微任务,没有的话执行下一个宏任务
5、执行setTimeout,setInterval 等宏任务的时候,如果里面有promise.then等代码的时候又要放入微任务队列尾部,执行完setTimeout,setInterval 后查看有没有要执行的微任务,没有的话执行下一个宏任务
6、 …

requestAnimationFrame、requestIdleCallback、事件循环、宏任务,微任务的关系

raf 在 render 中,requestIdleCallback 在 render 之后。raf在渲染之前执行的,requestIdleCallback 是在浏览器空闲时间才会执行,所以requestIdleCallback是最后执行的。
js事件循环机制EventLoop_第7张图片

可以用这个代码体验一下执行顺序


<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
  <style>
    #animation{
      
      background-color: aqua;
      width: 100px;
      height: 100px;
      border:1px solid rebeccapurple;
    }
  style>
head>
<body>
  <div id='animation'>animationdiv>

  <script>

window.requestIdleCallback(myNonEssentialWork);
const tasks = [
 () => {
      
   console.log("第一个任务");
 },
 () => {
      
   console.log("第二个任务");
 },
 () => {
      
   console.log("第三个任务");
 },
];

function myNonEssentialWork (deadline) {
      
  // 如果帧内有富余的时间,或者超时
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
      
    work();
  }

 if (tasks.length > 0) window.requestIdleCallback(myNonEssentialWork);

}

function work () {
      
 tasks.shift()();
//  console.log('执行任务');
}

var start = null;
var element = document.getElementById('animation');
element.style.position = 'absolute';

function step(timestamp) {
      
  console.log('222')
  if (!start) start = timestamp;
  var progress = timestamp - start;
  element.style.left = Math.min(progress / 10, 200) + 'px';
  // if (progress < 2) {
      
  //   window.requestAnimationFrame(step);
  // }
}
window.requestAnimationFrame(step);

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

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

console.log('console');

  script>
body>
html>

Chrome浏览器的执行顺序是

js事件循环机制EventLoop_第8张图片
js事件循环机制EventLoop_第9张图片

可以看到出现两种情况,requestAnimationFrame打印出来的222会在setTimeout之前或者之后,是因为raf是由系统来决定回调函数的执行时机的,会请求浏览器在下一次重新渲染之前执行回调函数,这个下一次重新渲染的时机我们不能固定,所以打印出来的顺序是不固定的。

举个

setTimeout(function() {
     
  console.log('setTimeout1');
})

new Promise(function(resolve) {
     
  console.log('promise');
  resolve();
}).then(function() {
     
  console.log('then');
})
setTimeout(function() {
     
  console.log('setTimeout2');
})

async function aaa() {
     
  console.log('async1')
  return 'async2'
}

async function test () {
     
  let a = await aaa();
  console.log('await',a)
}
test()
console.log('console');

这个代码打印出来的顺序你知道吗?
1、这一整块代码就是一个宏任务,先一行一行代码看,看到一个setTimeout,先把它放到宏任务队列的尾部标记为setTimeout1
2、然后遇到new Promise 这个时候就是立刻执行了,所以先输出“promise”
3、然后遇到了then,把它放入微任务队列的队尾
4、再往下,又遇到了setTimeout,再把它放到宏任务队列的尾部标记为setTimeout2,这个时候宏队列是这样的:
【setTimeout2】【setTimeout1】
5、再往下遇到两个函数,目前没有调用所以不用管
6、遇到了test的函数调用,就直接执行test内部,遇到await也是直接执行aaa(),所以输出“async1”,await这行后面的代码都是放入微任务队列尾部,这个时候微任务队列长这样:
【await async2】【then】
7、遇到最后一行代码了,这个时候直接输出“console”,至此最大的宏任务执行完毕。
8、这个时候查看有没有微任务,我们看到微任务队列【await async2】【then】,然后从队头取出微任务,所以依次输出thenawait async2
9、微任务队列为空后,说明执行完毕,然后查看有没有下一个宏任务
10、我们从宏任务队列的头部取出“setTimeout1”,输出“setTimeout1”,这个宏任务里面并没有微任务,所以这个setTimeout1宏任务结束
11、开始新的setTimeout2宏任务,**输出“setTimeout2”**后,这个宏任务里面并没有微任务,所以这个setTimeout2宏任务结束。
12、至此没有宏任务也没有微任务了,所以就结束了。依次输出的结果是

js事件循环机制EventLoop_第10张图片

小练习

看看你有没有掌握,下面几个输出的结果是什么呢?

PS:process.nextTick要在node环境才能运行出来

console.log('1');

process.nextTick(function() {
     
    console.log('6');
})

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

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


(async () => {
     

  console.log('1');
  await new Promise((resolve, reject) => {
     

    console.log('2');

    setTimeout(() => {
     

      console.log('3')
    }, 0)
    console.log('4')

  })
  console.log('5')
})();

公众号

欢迎大家关注我的公众号: 石马上coding,一起成长
石马上coding

参考:
1、李兵老师的浏览器工作原理与实践(这是一个付费专栏,以下链接不属于这个专栏)
2、https://juejin.im/post/6844903512845860872
3、https://www.jianshu.com/p/2771cb695c81
4、https://html.spec.whatwg.org/multipage/webappapis.html#event-loops
5、https://javascript.info/event-loop

你可能感兴趣的:(javascript,队列,eventloop,RAF,事件循环,宏任务微任务)