异步编程之事件循环机制

JavaScript 是一门单线程语言,咱们能够经过异步编程的方式来实现实现相似于多线程语言的并发操做。javascript

本文着重讲解经过事件循环机制来实现多个异步操做的有序执行、并发执行;经过事件队列实现同级多个并发操做的前后执行顺序,经过微任务和宏任务的概念来说解不一样阶段任务执行的前后顺序,最后经过将浏览器和 Node 下的事件循环机制进行对比,对比其事件循环机制的不一样之处,以及在 Node 端经过libuv引擎来实现多个异步任务的并发执行。java

1、前言

咱们知道JavaScript 是一门单线程语言,对于大多数人而言,单线程最大的好处是不用像多线程那样到处在乎状态的同步问题,这里没有死锁的存在,也没有像多线程之间来回切换带来性能上的开销。一样,单线程也存在自身的弱点,主要表如今如下几个方面:node

  1. 没法利用多核cpu,一个简单的例子,在一个位置从同一台服务器拉取不一样的资源,若是采用单线程同步的方式去拉取,代码大体以下:编程

    getData(‘from_db’),//耗时为M,
    getData(‘from_db_api’),//耗时为N,
    若是采用同步单线程的方式总共耗时为:M+N
  2. js代码错误或者耗时过长会阻塞后面代码的执行,例如页面在进行dom渲染时,若是页面的js代码报错会引发整个页面白屏的现象。api

  3. 大量计算占用CPU致使没法继续调用异步I/O。
    后来HTML5定制了Web Workers可以建立多线程来进行计算,可是使用Web Workers技术开的多线程有着诸多的限制,例如:全部新线程都受主线程的彻底控制,不能独立执行。这意味着这些“线程” 实际上应属于主线程的子线程。另外,这些子线程并无执行I/O操做的权限,只能为主线程分担一些简单的计算任务。因此严格来说这些线程并无完整的功能,也所以这项技术并不是改变了 JavaScript 语言的单线程本质。浏览器

    因此咱们能够预见,将来的 JavaScript 依然会是一门单线程语言,所以JavaScript采用异步编程方式实现程序“非阻塞”的特色,那么咱们如何实现这一特征了,答案就是咱们今天要讲的——event loop(事件循环)。服务器

2、浏览器下的事件循环机制

一、执行栈

JavaScript变量主要存储在堆和栈两个位置,其中,堆里主要存储对象,栈主要存储基本类型的变量以及指针变量。当咱们调用一个方法时,JS 会生成一个与这个方法对应的执行环境,又叫执行上下文,当一系列方法被调用时,因为咱们的js是单线程的,因此这些方法会被单独排在一个地方,这个地方叫作执行栈。
当一个脚本第一次执行的时候,JS  引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,而后从头开始执行。若是当前执行的是一个方法,那么 JS 会向执行栈中添加这个方法的执行环境,而后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,JS 会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。这个过程反复进行,直到执行栈中的代码所有执行完毕。网络

二、事件队列

以上说的都是 JS 同步代码的执行,那么当程序执行异步代码后会如何进行呢?咱们前面提到过 JS 最大的特色是非阻塞,下面咱们说一下实现这一点的关键在于这项机制——事件队列。多线程

当js引擎遇到一个异步事件后不会一直等待返回结果,这个事件会先挂起,继续执行执行栈中的其余任务,直到这个异步事件的结果返回,JS 引擎会将这个事件放入与当前执行栈不一样的一个队列中,咱们称之为事件队列。并发

被放入事件队列不会马上执行其回调,而是等待当前执行栈中的全部任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。若是有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,而后执行其中的同步代码...,如此反复,这样就造成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的缘由。

异步编程之事件循环机制

(图片来源:网络)

三、微任务和宏任务

关于微任务和宏任务咱们能够用一张图来讲明:

异步编程之事件循环机制

(图片来源:网络)

在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。而且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。若是不存在,那么再去宏任务队列中取出一个事件并把对应的回调加入当前执行栈;若是存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,而后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈...如此反复,进入循环。

宏任务主要包含:script( 总体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)

微任务主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)

3、Node环境下的事件循环模型

与浏览器有何异同?

在 Node 中,事件循环表现出的状态与浏览器中大体相同。不一样的是 Node  中有一套本身的模型。Node  中事件循环的实现是依靠的libuv引擎。咱们知道 Node  选择Chrome V8引擎做为js解释器,V8引擎将js代码分析后去调用对应的Node   api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不一样的事件放在不一样的队列中等待主线程执行。所以实际上 Node  中的事件循环存在于libuv引擎中。

异步编程之事件循环机制

(图片来源:网络)

从上面这个模型中,咱们能够大体分析出 Node  中的事件循环的顺序:

外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...

以上各阶段的名称是根据我我的理解的翻译,为了不错误和歧义,下面解释的时候会用英文来表示这些阶段。这些阶段大体的功能以下:

timers: 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。

  • I/O callbacks: 这个阶段执行几乎全部的回调。可是不包括close事件,定时器和setImmediate()的回调。
  • idle, prepare: 这个阶段仅在内部使用,能够没必要理会。
  • poll: 等待新的I/O事件,node在一些特殊状况下会阻塞在这里。
  • check: setImmediate()的回调会在这个阶段执行。
  • close callbacks: 例如socket.on('close', ...)这种close事件的回调。

4、小结

JavaScript事件循环是很是重要的一个基础概念,咱们能够经过这种机制实现异步编程,解决JavaScript同步单线程没法实现并发操做的问题,可使咱们对一段异步代码的执行顺序有一个清晰的认识,从而减小代码运行的不肯定性。合理的使用各类延迟事件的方法,有助于代码更好的按照其优先级去执行。

做者:Liu Gang

相关文章
相关标签/搜索