js Event Loop 事件循环

JavaScript08

js Event Loop 事件循环,第1张

Event Loop即事件循环,是解决javaScript单线程运行阻塞的一种机制。 主要是为了协调单线程下,事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞。

因为JavaScript 是单线程,也就是说, 所有任务需要排队,前一个任务结束,才会执行后一个任务。

但是IO设备(输入、出设备)可能会因为网络等因数导致速度很慢(比如Ajax)继而CPU没有充分利用,所以设计者将IO设备的任务挂起,先执行后面的任务,等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。于是,就把所有任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

只有前一个任务执行完毕,才能执行后一个任务;直接在主线程上排队执行且最先执行,形成一个执行栈

不进入主线程、而是进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",执行一个宏任务, 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中 宏任务执行完毕后,再依次执行执行当前微任务队列中的所有微任务,当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

(4)主线程不断重复上面的第三步。

"任务队列"是一个先进先出的数据结构,也是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

例子1

例子2:

例子3:

nodejs事件循环和浏览器的事件循环不一样的。浏览器的Event loop是在HTML5中定义的规范,而node中则由libuv库实现

这其实就要说到js的单线程和多线程的理论了,其实,js是单线程的,但从某种意义上来说,也可以说是多线程的,就像你说的 【事件队列】和【执行栈】,其实js是只有一个主线程,也就相对于的产生了【执行栈】,然后每一个子线程也就产生各自对应的【事件队列】

JS运行的环境称之为宿主环境

执行栈: call stack ,一个数据结构,用于存放各种函数的执行环境,每一个函数执行之前,它的相关信息会加入到执行栈。函数调用之前,创建执行环境,然后加入到执行栈;函数调用之后,销毁执行环境。

JS引擎永远执行的是执行栈的最顶部。

异步函数:某些函数不会立即执行,需要等到某个时机到达后才会执行,这样的函数称之为异步函数。比如事件处理函数。异步函数的执行时机,会被宿主环境控制。

浏览器宿主环境中包含5个线程:

当上面的线程发生了某些事情,如果该线程发现,这件事情也有处理程序,它会将该处理程序加入一个叫做事件队列的内存。当JS引擎发现,执行栈中已经没有了任何内容后,会将事件队列中的第一个函数加入到执行栈中执行。

JS引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环。

事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:

MutationObserver用于监听某个DOM对象的变化

当执行栈清空时,JS引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。

我们习惯于使用传统的回调或事件处理来解决异步问题

事件:某个对象的属性是一个函数,当发生某一件事时,运行该函数

回调:运行某个函数以实现某个功能的时候,传入一个函数作为参数,当发生某件事的时候,会运行该函数

本质上,事件和回调并没有本质的区别,只是把函数放置的位置不同而已。

一直以来,该模式都运作良好。

目前,该模式主要面临以下两个问题:

ES官方参考了大量的异步场景,总结出了一套异步的通用模型,该模型可以覆盖几乎所有的异步场景,甚至是同步场景

值得注意的是,为了兼容旧系统,ES6并不打算抛弃掉过去的做法,只是基于该模型推出一个全新的API,使用该API,会让异步处理更加的简洁优雅。

理解该API,最重要的是理解它的异步模型

事情总是从 未决阶段 逐步发展到 已决阶段 的。并且,未决阶段拥有控制何时通向已决阶段的能力。

既然未决阶段有权利决定事情走向,因此,未决阶段可以决定事情最终的状态

我们将把事情变为 resolved 状态的过程叫做: resolve ,推向该状态时,可能会传递一些数据

我们将把事情变为 rejected 状态的过程叫做: reject ,推向该状态时,同样可能会传递一些数据,通常为错误信息

后续处理可能有多个,因此会形成作业队列,这些后续处理会按照顺序,当状态到达后依次执行

细节

当后续的 Promise 需要用到之前的Promise的处理结果时,需要 Promise 串联

Promise 对象中,无论时 then 方法还是 catch 方法,它们都具有返回值,返回的是一个全新的 Promise 对象,它的状态满足下面的规则:

后续的 Promise 一定会等到前面的 Promise 有了后续处理结果后,才会变成已决状态

async 和 await 是ES2016新增的两个关键字,它们借鉴了ES2015 中生成器的实际开发中的应用,目的是简化Promise API 的使用,并非是替代了 Promise

async 用于修饰函数(无论是函数字面量还是函数表达式),放置在函数最开始的位置, 被修饰的函数返回结果一定是 Promise 对象。

await关键字必须出现在async函数中

await用在某个表达式之前,如果表达式是一个 Promise ,则得到的是 thenable 中的状态数据