深入理解事件循环机制

Posted 前端一锅煮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解事件循环机制相关的知识,希望对你有一定的参考价值。

微信公众号:[前端一锅煮]
一点技术、一点思考。
问题或建议,请公众号留言。

抛在前面的问题:

js 是单线程的如何做到异步?

事件循环的过程是怎样的?

macrotask 和 microtask 是什么,它们有何区别?

node.js 的事件循环是怎样的,和浏览器的事件循环有何区别?

进程和线程

浏览器是多进程的,具体包含的进程有:

  • Browser 进程:浏览器的主进程(负责协调、主控),只有一个;
  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建;
  • GPU 进程:最多一个,用于 3D 绘制;
  • 浏览器渲染进程(内核):默认每个 tab 页面一个进程,互不影响,控制页面渲染,脚本执行,事件处理等(有时候会优化,如多个空白 tab 会合并成一个进程)。

其中浏览器渲染进程就是前端页面主要用到的进程,包含的线程有:

  • GUI 渲染线程(负责渲染页面,解析 html,CSS 构成 DOM 树,和 JS 引擎互斥)
  • JS 引擎线程
  • 事件触发线程
  • 定时器触发线程
  • Http 请求线程等主要线程

关于执行中的线程:

主线程:也就是 js 引擎执行的线程,此线程只有一个,页面渲染、函数处理都在这个主线程上执行。

工作线程:也称幕后线程,这个线程可能存在于浏览器或 js 引擎内,与主线程是分开的,处理文件读取、网络请求等异步事件。

事件循环

所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行。而异步任务,就是异步执行的任务,比如 ajax 网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列的机制(先进先出的机制)来进行协调。

同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入任务队列。主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就是我们说的 Event Loop (事件循环)。

在事件循环中,每进行一次循环操作称为 tick,其关键的步骤可以总结如下:

  1. 一开始整个脚本作为一个宏任务执行
  2. 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  3. 当前宏任务执行完出队,读取微任务列表,有则依次执行,直到全部执行完
  4. 读取宏任务列表,有则依次执行,直到全部执行完
  5. 执行浏览器 UI 线程的渲染工作
  6. 检查是否有 Web Worker 任务,有则执行
  7. 执行完本轮的宏任务,回到第 2 步,继续依此循环,直到宏任务和微任务队列都为空

宏任务与微任务

JS 引擎把所有任务分成两类,一类叫宏任务(macrotask),一类叫微任务(microtask)。

宏任务主要包含:

  • js(整体代码)
  • I/O、UI 渲染
  • MessageChannel、postMessage
  • setImmediate(Node.js 环境)
  • setTimeout、setInterval
  • requestAnimationFrame 属于 GUI 引擎,发生在渲染过程的重绘重排部分,在 UI 渲染之前执行

微任务主要包含:

  • process.nextTick(Node.js 环境)
  • MutaionObserver(浏览器环境)
  • Promise

先执行完微任务再执行宏任务。

Node.js 的事件循环

事件循环是 Node.js 处理非阻塞 I/O 操作的机制。目前大多数内核都是多线程的,它们可在后台处理多种操作。当其中的一个操作完成的时候,内核通知 Node.js 将适合的回调函数添加到轮询队列中等待时机执行。

当 Node.js 启动后,它会初始化事件循环,处理已提供的输入脚本,它可能会调用一些异步的 API、调度定时器,或者调用 process.nextTick(),然后开始处理事件循环。

以下是 Node.js 事件循环顺序:

  1. timers:执行到期的 setTimeout、setInterval 回调。
  2. pending callbacks:挂起的回调函数,执行延迟到下一个循环迭代的 I/O 回调。对某些系统操作(如TCP错误类型)执行回调。
  3. idle, prepare:空闲 准备,node 系统内部使用。
  4. poll:检索新的 I/O 事件。执行 I/O(例如文件、网络)的回调,除了 close,定时器和 setImmediate 以外的所有回调。其余情况 node 将在适当的时候在此阻塞。
  5. check:执行 setImmediate 回调。
  6. close callbacks:执行 close 事件回调,如 socket.on(\'close\', ...)、http close等。

上面是宏任务的执行顺序,Node.js 也是先执行微任务再执行宏任务。微任务主要有 process.nextTick 和 promise,其中 process.nextTick 先执行。

最后,javascript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。

以上是关于深入理解事件循环机制的主要内容,如果未能解决你的问题,请参考以下文章

vue nextTick深入理解-vue性能优化DOM更新时机事件循环机制

深入理解JavaScript的事件循环(Event Loop)

深入理解js Dom事件机制——添加事件处理程序

对JS事件机制的深入理解

深入理解Spring的容器内事件发布监听机制

深入理解Spring的容器内事件发布监听机制