前端面试系列-JavaScript中的Event Loop(事件循环)机制(含图解)

1、前言

javascript是一门单线程非阻塞的脚本语言。javascript

单线程

单线程:javascript代码在执行的任什么时候候,都只有一个主线程来处理全部的任务。java

单线程的缘由:浏览器中,咱们须要进行各类各样的dom操做。若是javascript是多线程的,那么当两个线程同时对dom进行一项操做,例如一个向其添加事件,而另外一个删除了这个dom,此时该如何处理呢?所以,为了保证不会 发生相似情景,javascript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性node

单线程在保证了执行顺序的同时也限制了javascript的效率,所以开发出了web worker技术。这项技术号称让javascript成为一门多线程语言。可是全部新线程都受主线程的彻底控制,不能独立执行。这意味着这些“线程” 实际上应属于主线程的子线程。这些子线程并无执行I/O操做的权限,只能为主线程分担一些诸如计算等任务。因此严格来说这些线程并无完整的功能,也所以这项技术并不是改变了javascript语言的单线程本质。web

非阻塞

当代码须要进行一项异步任务(没法马上返回结果,须要花必定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,而后在异步任务返回结果的时候再根据必定规则去执行相应的回调。chrome

javascript引擎实现非阻塞的关键就是——event loop(事件循环)api

2、浏览器环境下js引擎的事件循环机制

1.执行栈与任务队列

当javascript代码执行的时候会将不一样的变量存于内存中的不一样位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。promise

执行上下文

当咱们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有做用域,上层做用域的指向,方法的参数,这个做用域中定义的变量以及这个做用域的this对象。浏览器

执行栈

当一系列方法被依次调用的时候,由于js是单线程的,同一时间只能执行一个方法,因而这些方法被排队在一个单独的地方。这个地方被称为执行栈。markdown

当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,而后从头开始执行。若是当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,而后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。这个过程反复进行,直到执行栈中的代码所有执行完毕。多线程

栈溢出

一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境中还能够调用其余方法,甚至是本身,其结果不过是在执行栈中再添加一个执行环境。这个过程能够是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值。

任务队列(Task Queue)

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

事件循环(Event Loop)

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

在这里插入图片描述

2.微任务(micro task)和宏任务(macro task)

如下事件属于宏任务:

  • 总体Script代码
  • setInterval()
  • setTimeout()
  • setImmediate()
  • promise声明里面的代码

如下事件属于微任务

  • new MutaionObserver()
  • Promise的then
  • process.nextTick

在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。若是不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;若是存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,而后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈...如此反复,进入循环。

当当前执行栈执行完毕时会马上先处理全部微任务队列中的事件,而后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务以前执行。

在这里插入图片描述 例子:

setTimeout(function () {
    console.log(1);
});

new Promise(function(resolve,reject){
    console.log(2)
    resolve(3)
}).then(function(val){
    console.log(val);
})
console.log(4);
//2
//4
//3
//1
复制代码

3、node环境下的事件循环机制

1.与浏览器环境的差别

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

2.事件循环模型

一个libuv引擎中的事件循环的模型:

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
*注:模型中的每个方块表明事件循环的一个阶段*
复制代码

这个模型是node官网上的一篇文章中给出的。

3.事件循环各阶段详解

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

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

这些阶段大体的功能以下:

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

参考连接

相关文章
相关标签/搜索