事件循环,如此简单

1,614 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

事件循环(Event Loop)

事件循环是---指浏览器或者Node解决JavaScript单线程运行时不会堵塞的一种机制

  • 浏览器或者Node

    也就是说根据JavaScript的运行环境,会有一些不同的地方,这个放到后面介绍

  • JavaScript单线程(一个主线程)

    为什么不能设计成多线程语言呢?JavaScript设计出来时是为了处理用户交互、网络和操作DOM等需求的,如果是多线程会带来很复杂的同步问题,假设有两个主线程,一个线程在某个DOM节点添加内容,另一个线程删除这个节点,这个时候JavaScript应该怎么办?

  • 不会堵塞

    那既然是单线程,那前面的事件如果超级耗时,那不就堵塞了么?要一直等待么?所以JavaScript事件循环中异步的概念就是为了解决阻塞问题,也是事件循环的核心知识

调用栈(Call Stack)

所有的任务都会被放到调用栈,在调用栈中按照顺序等待主程序依次执行

  • 所有任务

    包括同步任务和异步任务

    • 同步任务---在主程序上排队执行的任务(一般来说调用之后很快就能得到结果,我们采取同步策略)
    • 异步任务---等主程序上同步任务全部执行完毕才执行的任务(一般来说调用之后需要过一段时间才能的到结果,我们采取异步策略)
  • 什么时候放到调用栈的

    JS引擎按照顺序解析代码,遇到函数调用,入栈 (遇到函数声明,入堆)

  • 调用栈

    后进先出的有序集合,这是代码执行的地方

  • 主程序

    JS引擎,负责处理 JavaScript脚本,执行代码

  • 执行

    • 同步函数执行顺序----入栈,直接执行,执行完得到结果后弹出栈,继续下一个函数调用
    function one(){
      console.log("执行1");
      console.log("执行2");
    }
    one()
    

    image-20221124151034494.png

    • 异步函数执行顺序----入栈,分给Web APIs,弹出栈,继续下一个函数调用

      Web API:是浏览器提供的一套操作浏览器功能和页面元素的API(DOM和BOM,其中包含处理JS异步的方法)。Web API一般都有输入和输出(函数的传参和返回值),Web API很多都是方法(函数)

      image-20221124150439225.png 处理异步任务时,入栈之后提交给对应的异步API处理,然后出栈

    注:异步API(web API中处理异步的API,比如说 事件监听函数、DOM、HTTP/AJAX请求、setTimeout等等)

    function two() {
      setTimeout(() => {
        console.log("执行1");
      }, 100);
      setTimeout(() => {
        console.log("执行2");
      }, 100)
    }
    two()
    

    image-20221124152610220.png

    • 流程图

    image-20221124161643652.png

异步处理流程

交给异步API处理的异步任务,会有一个什么样的流程呢?

简单来说,异步任务不是连续完成的,先执行第一段,等第一段执行完放入队列做好准备,等主程序执行第二段,第二段也被叫做回调。

image-20221125155935455.png

任务队列

按照先进先出的顺序存储着所有异步任务的回调函数,任务队列分为两种:宏任务队列(Task Queue)微任务队列(Microtask Queue) ,对应的里面存放的是宏任务微任务

为什么要分宏任务和微任务?

  • 宏任务进程之间的切换,速度慢,且每次执行需要切换上下文。因此一个Eventloop中只执行一个宏任务。由宿主发起,根据环境不同,宏任务由浏览器或node发起的。

  • 微任务线程之间的切换,速度快。不用进行上下文切换,可以快速的一次性做完所有的微任务。微任务是JavaScript自身发起的。

    在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,需要浏览器来做,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了,也就有了微任务。据此也可以知道宏任务和浏览器有关,微任务和JavaScript自身有关

什么是进程和线程?

我们前面说的JS是单线程,指的是一个进程里只有一个主线程

官方的说法是:进程是CPU资源分配的最小单位;线程是CPU调度的最小单位;

image-20221125110322439.png

  1. 每个进程都有单独属于自己的CPU资源
  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
  3. 每个进程都是独立存在的
  4. 每个线程都可以用自己所属进程的资源,因为一个进程内的内存空间是共享的

怎么分辨宏任务和微任务?

宏任务是由宿主发起的,而微任务是由 JS 本身发起的

宏任务浏览器Node
整体代码(script)
setTimeout
setInterval
setImmediate
异步Ajax请求
I/O
UI交互事件
requestAnimationFrame
postMessage
MessageChannel
微任务浏览器Node
Promise.then 、catch、finally
MutationObserver(对 Dom 变化监听)
process.nextTick

事件循环中宏任务和微任务怎么执行?

image-20221125145829763.png 当调用栈为空时,主程序就检查任务队列中是否有任务,如果有任务就按队列顺序入栈运行,这个过程不断重复,就是事件循环;

由于任务队列有宏任务队列和微任务队列两种,宏任务和微任务会有什么不一样的操作呢?

image-20221126104456532.png

  • 每次循环都会把微任务队列里的任务执行完的
  • 每次循环都只会执行宏任务队列第一个任务
  • 每执行完一个宏任务执行,调用栈就是空的,就会检查微任务队列是否有任务,如果有则把微任务执行完,再执行下一个宏任务,如果没有则直接执行下一个宏任务
  • 由于前面说到script整体代码也是一个浏览器宏任务,当我们第一次执行时,主程序就从宏任务队列取script宏任务开始执行,然后执行宏任务下的所有同步任务,script宏任务结束执行,调用栈为空,执行所有微任务,执行下一个宏任务......
  • 如果在执行微任务的过程中,产生新的微任务添加到微任务列表,也需要一起清空;微任务没清空之前,不会执行下一个微任务
  • 初始的时候,调用栈是空的,微任务队列也是空的,只有宏任务队列有任务,所以......

看懂这块代码基本就懂了

<!DOCTYPE html>
<html lang="en"><head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head><script>
  function first() {
    console.log("first开始");
​
    setTimeout(() => {
      console.log("开始第二轮宏任务");
      new Promise((resolve, reject) => {
        resolve("执行第二轮宏任务后的微任务1")
      }).then((res => {
        console.log(res);
        new Promise((resolve, reject) => {
          resolve("这是执行第二轮宏任务种微任务1时,所产生的新的微任务,也会在本次执行完再执行下一轮宏任务")
        }).then((respone => {
          console.log(respone);
        }))
​
      }))
​
      function reoloveTest() {
        console.log('reolove是同步的哦,不要搞错了哦');
        return "执行第二轮宏任务后的微任务2"
      }
​
      new Promise((resolve, reject) => {
        resolve(reoloveTest())
      }).then((res => {
        console.log(res);
      }))
​
      setTimeout(() => {
        console.log("开始第四轮宏任务");
        console.log("结束第四轮宏任务,没有微任务了,也没有宏任务了,结束");
      })
​
      console.log("结束第二轮宏任务,有微任务,准备执行微任务");
​
    }, 0);
​
    setTimeout(() => {
      console.log("开始第三宏任务");
      Promise.resolve(console.log("再次提醒resolve里的代码是同步任务哦"))
      new Promise((resolve, reject) => {
        resolve("执行第三轮宏任务后的微任务1")
      }).then((res => {
        console.log(res);
      }))
​
      new Promise((resolve, reject) => {
        resolve("执行第三轮宏任务后的微任务2,有宏任务,准备执行宏任务")
      }).then((res => {
        console.log(res);
      }))
      console.log("结束第三轮宏任务,有微任务,准备执行微任务");
    }, 0);
​
    new Promise((resolve, reject) => {
      resolve("执行微任务1")
    }).then((res => {
      console.log(res);
    }))
​
    console.log("first结束");
  }
​
  function second() {
    console.log("second开始");
    new Promise((resolve, reject) => {
      resolve("执行微任务2,微任务执行完,取宏任务执行")
    }).then((res => {
      console.log(res);
    }))
    console.log("second结束");
​
  }
​
​
  console.log("开始第一轮script宏任务");
  first()
  second()
  console.log("结束第一轮script宏任务,调用栈空,检查有微任务,取微任务执行");
​
</script><body>
</body></html>

上面代码有基础需要注意

  • Promise.resolve(console.log("再次提醒resolve里的代码是同步任务哦"))是同步的,相当于awit console.log("再次提醒resolve里的代码是同步任务哦"),即awit后面接的任务是同步任务

      async function asyncFn() {
        console.log('async start'); 
        await logInfo()
        console.log('async end');
      }
      function logInfo() {
        console.log('年轻人,千万不要赌球');
      }
      asyncFn()
      console.log("记住啊,千万别赌球");
    ​
    相当于
    ​
      function asyncFn() {
        console.log('async start');
        new Promise((resolve, reject) => {
          resolve(logInfo())
        }).then(() => {
          console.log('async end');
        })
      }
    ​
      function logInfo() {
        console.log('年轻人,千万不要赌球');
      }
    ​
      asyncFn()
      console.log("记住啊,千万别赌球");
    
  • 执行微任务的过程中,产生新的微任务添加到微任务列表,也需要一起清空;微任务没清空之前,不会执行下一个微任务

一些疑问

为何和定时器有关的任务是宏任务?

因为计时是实时的,它必定不能被阻塞,时器被设计在另外一个进程中被管理,因此,定时器任务会有进程的切换,所以是宏任务

把定时器想成一个时间管理中心,而后在上面注册一个个任务,这些任务自己和时间无关,时间管理中心和时间有关的,当时间管理中心发现时间到了,要执行任务,就从任务列表中找出注册的任务,并通知JS执行任务,因此能够看到,时间管理中心(定时器的进程)和执行的任务(JS运行时)是无关的,不共享上下文,因此是宏任务

事件为何是宏任务呢?

事件的触发是依赖于浏览器的实现,平台有它本身的事件注册和派发机制,事件的独立注册表和派发机制致使,他也不会和JS存在一个进程中,事件的管理中心必定是在另一个进程中实现的,也就是宏任务

那么像Observer(如MutationObserver等),和一些渲染为何都是微任务呢?

虽然它们和JS的自己无关,可是它们的执行时机和它们所在的进程是有关的

好比MutationObserver,观察的是DOM,它的做用便是对DOM的变化作出响应,因此,他会在管理DOM的进程中

渲染也是同样的,是在整个渲染流程中的某一步做的回调,并无切换出它的自己所在的空间

微任务在执行时,它能获取到任务外的上下文,宏任务在执行时,他不能获取到任务外的上下文

浏览器和Node的事件循环差异

下次一定