解释一下JS中的异步IO、回调、Eventloop

537 阅读4分钟

解释一下JS中的异步IO、回调、Eventloop

这是我参与2022首次更文挑战的第17天,活动详情查看:2022首次更文挑战」。

什么是IO?什么是同步和异步

线程在执行中如果遇到磁盘读写或网络通信(统称为 I/O 操作),通常要耗费较长的时间,这时线程将会将会进入阻塞状态,从而不会继续执行下一步操作。当 I/O 操作完毕时,操作系统将这个线程的阻塞状态解除,让它继续执行下去。这种现象我们称之为阻塞现象或者同步模式。

相应的也会有非阻塞模式也就是异步模式。异步模式就是当线程遇到 I/O 操作时,不会以阻塞的方式等待 I/O 操作的完成或数据的返回,计算机会继续执行下面的操作。当I/O操作完成时,通常会通过事件的形式通知到I/O操作线程,这种形式我们往往称之为回调。

那么当同步代码执行完了,我们就该去处理异步代码的回调了。这里就涉及到了几个问题:什么时候会开始执行回调任务,假如有多个回调又是怎么分配的,这其中又是通过什么来协调的?其中事件循环扮演者一个总调度师的角色。

什么是事件循环 (Eventloop)

时间循环主要有三个部分组成:执行堆,执行栈和任务队列。

执行栈(stack):这个栈区域存放了所有的任务,在栈顶的就是当前执行的任务。

执行堆(heap): 用来表示内存的,当任务被创建时,会产生一些变量,这些变量就会被储存在这里。当任务执行完毕后,变量将在堆中被销毁。

任务队列(quene):当产生异步I/O时,异步环境下执行栈不会等待操作的结束,而是继续执行后面的任务。那么当异步任务完成时会产生回调,这个回调任务将会被放入队列中。

这三个部分是如何协同工作的?

一张图带你弄懂什么是事件循环

1.gif

  1. 在执行栈中可能有多个任务,JS是单线程的,在执行栈中一次只会执行一个任务。
  2. 在任务创建时会在堆中创建对应环境中的变量,在任务执行完成时被销毁。
  3. 当在异步环境下时,不会阻塞栈中后面任务的执行,当异步任务执行完成时销毁变量,并且创建回调任务放入队列中。
  4. 当线程中的任务执行完毕,就会开始执行队列中的任务。
  5. 不断重复以上操作,只要线程空闲就会去队列中看看是否有任务待执行。

宏任务和微任务

通常我们把异步任务分为宏任务与微任务,它们的区分在于:

  • 宏任务(macro-task):一般是 JS 引擎和宿主环境发生通信产生的回调任务,比如 setTimeout,setInterval 是浏览器进行计时的,其中回调函数的执行时间需要浏览器通知到 JS 引擎,网络模块, I/O处理的通信回调也是。包含有 setTimeout,setInterval,DOM事件回调,ajax请求结束后的回调,整体 script 代码,setImmediate。
  • 微任务(micro-task):一般是宏任务在线程中执行时产生的回调,如 Promise,process.nextTick,Object.observe(已废弃), MutationObserver(DOM监听),这些都是 JS 引擎自身可以监听到回调。

上面我们了解了宏任务与微任务的分类,那么为什么我们要将其分为宏任务与微任务呢?主要是因为其添加到事件循环中的任务队列的机制不同。

在事件循环中,任务一般都是由宏任务开始执行的(JS代码的加载执行),在宏任务的执行过程中,可能会产生新的宏任务和微任务,这时候宏任务(如ajax回调)会被添加到任务队列的末尾等待事件循环机制执行,而微任务则会被添加到当前任务队列的前端,也是等待事件循环机制的执行。

其中相同类型的宏任务或微任务会按照回调的先后顺序进行排序,而不同任务类型的任务会有一定的优先级,按照不同类型任务区分

宏任务优先级,主代码块 > setImmediate > MessageChannel > setTimeout / setInterval

微任务优先级,process.nextTick > Promise > MutationObserver