event loop

一、 同步与异步

同步和异步是指,请求方获取消息的结果主动发起的,还是被动等通知的。
若是请求方主动发起的,一直在等待应答结果(同步阻塞),
或者可以先去处理其他的事情,但要不断轮询查看发起的请求是否有应答结果(同步非阻塞 )
因为不管如何,都要发起方主动获取消息结果,所以形式上还是同步操作。
如果是由服务方通知的, 也就是
请求方发出请求后,要么在一直等待通知(异步阻塞),
要么就先去干自己的事了(异步非阻塞),
当事情处理完成之后,服务方会主动通知请求方,它的请求已经完成,这就是异步。

二、 阻塞与非阻塞

调用了一个函数之后,在等待这个函数返回结果之前,
当前的线程是处于挂起状态,还是运行状态。
如果是挂起状态,就意味着当前线程什么都不能干,就等着获取结果,这就叫同步阻塞
如果仍然是运行状态,就意味当前线程是可以的继续处理其他任务,但要时不时的去看下是否有结果了,这就是同步非阻塞

三、事件循环

JavaScript是单线程的语言。
单线程的缺点是任务执行会阻塞。
JavaScript用异步回调(asynchronous callback)来解决这些问题。
实现异步回调的特性,其实是基于Event Loop(事件循环)。

预备知识

一、 浏览器的线程

JS引擎是单线程的,但是浏览器是多线程的。
现代浏览器的一个 tab ,其中的线程包括但不局限于:

  • GUI 渲染线程
    负责渲染浏览器界面
  • JS引擎线程
    负责处理脚本
  • 事件触发线程
  • 定时器触发线程
  • 异步http请求线程
    JavaScript中的异步回调是通过 WebAPIs 去支持的,
    常见的有 XMLHttpRequest,setTimeout,事件回调(onclik, onscroll等)。
    这几个 API ,浏览器都提供了单独的线程去运行,
    所以才会有合适的地方去处理定时器的计时、各种请求的回调,与JS引擎不在同一个线程。
    另外,
    GUI 渲染和JavaScript执行是互斥的。因为JavaScript执行结果可能会对页面产生影响。
    所以浏览器对此做了处理,
    大部分情况下JavaScript线程执行,执行渲染(render)的线程就会暂停,等JavaScript的同步代码执行完再去渲染。

二、Event Loop (事件循环) 的定义

Event Loop 是让 JavaScript 做到既是单线程,又绝对不会阻塞的、实现异步回调的一种机制。
用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等。
Event Loop的作用很简单: 监控调用栈和任务队列。
如果调用栈是空的,它就会取出队列中的第一个”callback函数”,然后将它压入到调用栈中,然后执行它。

三、内存模型

从JS内存模型的角度,可将内存划分为调用栈(Call Stack)、堆(Heap)以及任务队列(Queue)等几个部分。
调用栈会记录所有的函数调用信息。
则存放了大量的非结构化数据,如程序分配的变量与对象。
任务队列包含了一系列待处理的信息及相关的回调函数。
任务队列又分为 MacroTask Queue 和 MicroTask Queue 两种。

四、 MacroTask 和 MicroTask

MacroTask Queue(宏任务队列)

Event Loop 会有一个或多个 MacroTask Queue,
这是一个先进先出(FIFO)的有序列表,
存放着来自不同 Task Source(任务源)的 Task(也即MacroTask)。

MacroTask Source 的定义

常见的宏任务: 鼠标、键盘事件,AJAX,数据库操作(例如 IndexedDB),以及定时器相关的 setTimeout、setInterval 等等
都属于 Task Source,
所有来自这些 MacroTask Source 的 MacroTask 都会被放到对应的 MacroTask Queue 中等待处理。

MacroTask、MacroTask Queue 和 Task Source 的规定

来自相同 Task Source 的 MacroTask,必须放在同一个 MacroTask Queue 中;
来自不同 Task Source 的 MacroTask,可以放在不同的 MacroTask Queue 中;
同一个 MacroTask Queue 内的 MacroTask 是按顺序执行的;
但对于不同的 MacroTask Queue(Task Source),浏览器会进行调度。
例如,
鼠标、键盘事件和网络请求都有各自的 MacroTask Queue,
当两者同时存时,浏览器为了保证流畅的用户体验,
优先从与用户交互相关的 MacroTask Queue 中挑选 MacroTask 并执行,比如鼠标、键盘事件。

MicroTask Queue(微任务队列)

一个 Event Loop 只有一个 MicroTask Queue。
Promise

事件循环过程

Event Loop中,每一次循环任务如下:
调用栈选择最先进入队列的MacroTask(通常是script整体代码),如果有则执行;
检查是否存在 MicroTask,如果存在,则不停的执行,直至清空 MicroTask Queue;
浏览器更新渲染(render),每一次事件循环,浏览器都可能会去更新渲染;
重复以上步骤。

注意:

一个Event Loop会有一个或多个 MacroTask Queue,但仅有一个 MicroTask Queue。
每个 MacroTask Queue 都保证按照入队列的顺序,依次执行MacroTask,在 MacroTask 或者 MicroTask 中产生的新 MicroTask 会被压入到 MicroTask Queue中。
在执行下一个MacroTask之前,会优先清空已有的 MicroTask Queue。

事件循环运行机制

从相对宏观的角度,简化运行步骤如下:

  1. 主线程不断循环;
  2. 对于同步任务,按顺序进入调用栈;
  3. 对于异步任务:
    与步骤 2 相同,执行同步代码;
    将相应的 MacroTask(或 Microtask)添加到任务队列,并由其他线程来执行具体的异步操作。

tips:
其他线程是指:JavaScript 是单线程,但浏览器内核是多线程的,它会将 GUI 渲染、定时器触发、HTTP 请求等工作交给专门的线程来处理

  1. 当主线程执行完当前调用栈中的所有任务,就会去读取任务队列,取出并执行;
  2. 重复以上步骤。

举例

拿 setTimeout 举例:

  1. 主线程同步执行这个 setTimeout 函数本身。
  2. 将负责执行这个 setTimeout 的回调函数的 MacroTask 添加到 MacroTask Queue。
  3. 定时器开始工作(实际上是靠 Event Loop 不断循环检查系统时间来判断是否已经到达指定的时间点)。
  4. 主线程继续执行其他任务。
  5. 当调用栈为空,且定时器触发时,主线程取出 MacroTask 并执行相应的回调函数。
    显然,执行 setTimeout 不会导致阻塞。
    但是,如果主线程很忙的话(调用栈一直非空),就会出现明明时间已经到了,却也不执行回调的现象,所以类似 setTimeout 这样的回调函数都是没法保证执行时机的。
查看评论