[面试整理]什么是EventLoop?

219 阅读6分钟

前言

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

参考视频:【中文字幕】Philip Roberts:到底什么是Event Loop呢?(JSConf EU 2014)_哔哩哔哩_bilibili

参考文章:

The event loop - JavaScript | MDN (mozilla.org)

JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志 (ruanyifeng.com)

理论模型

首先我们先来看一下javascript的理论模型(当时视频中介绍的是:chromeV8的引擎):

理论模型.png

这边我们可以看到,一共有三给区域

  • Stack:这个是,当函数被调用的时候,那么我们这个函数的帧Frame就会进入这个栈,并且这个Frame里面是会包括这个函数的很多内容(参数等),当我们这个函数执行完成以后(return)以后,我们才会将这个Frame释放掉

  • Queue:这个就是用于存储Message的地方,每一个Message都会关联着一个函数

    意思就是:当我们这个Message被处理,那么首先MessageQueue里面shift出去,然后,Message会引来一个函数,那么这个函数unshift进入,然后执行,然后执行完,pop出去

  • Webapis:我的理解是:浏览器进行处理的地方,比如网络请求setTimeout等,等这个浏览器处理完了,那么就会进入队列Queue成为一个Message,等我们将stack里面处理完了,那么我们才会将这个Message放入Stack

代码分析

首先拿MDN上的一个例子来说明:

function foo(b) {
  let a = 10;
  return a + b + 11;
}
function bar(x) {
  let y = 3;
  return foo(x * y);
}
console.log(bar(7));

先看运行状态:latentflip.com/loupe/?code…

理解:

  • 我们可以很清晰的看到,首先bar(7)这个函数被调用,那么想进入Stack
  • bar()里面,因为调用了foo(),所以foo()函数进入
  • 这时候foo()函数执行,并且具有返回值,那么foo()出栈
  • 这时候,我的bar()也就有了返回值42,然后出栈
  • 最后整个结束

那么我们现在来看一下setTimeout

栈溢出

所以这就可以解释,我们之前可能遇到的栈溢出问题:Maximum call stack size exceeded

function cb(){
    return cb()
}
cb()

这个函数很明显,他就是会栈溢出

  • 因为它一直在调用cb这个函数,这个函数会一直进入到Stack里面
  • 直到满为止

阻塞

视频的解释很好理解:当我们在栈中执行一段很慢的东西的时候我们就会产生阻塞

为什么?

首先,我们必须知道js是一个单线程语言,我是这么理解的,也就是,你在这个函数还没有执行完成以后,其他的函数是无法影响这个函数的执行的,他只能等待,等我将这个函数执行完了,你才能执行

var foo = $.getSync("//foo.com")
var bar = $.getSync("//bar.com")
var qux = $.getSync("//qux.com")
console.log(foo)
console.log(bar)
console.log(qux)

这时候我们会看到,我们的栈中,首先进入foo,当foo执行完成,shift结束了,我们的bar才会进入,我们的foo执行了很长一段时间,这时候我们的barqux就只能等待,所以,产生了阻塞

setTimeout

setTimeout:其实是一个异步的函数他接受两个参数,一个是回调函数,一个是time

setTimeout(function(){
    console.log("hello,summer")
},1000);

这段代码大家应该都很熟悉:就是一秒以后,输出hello,summer

当时这么说,其实并不准确,因为,这里的message,并不是指的是:等待的准确时间,其实它是指等待的最少事件

什么意思?

意思就是:当我们的stack里面是非空的时候,我们的setTimeOut里面的回调函数,即使到了那个time,也不会执行,他会变为一个message进入Queue进行等待,当我们Stack空了,那个回调函数才会执行

先看一段代码(来自MDN

const s = new Date().getSeconds();
setTimeout(function cb() {
  // 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
  console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);
while(true) {
  if(new Date().getSeconds() - s >= 2) {
    console.log("Good, looped for 2 seconds");
    break;
  }
}

先看是如何运行的:latentflip.com/loupe/?code…

我们可以看到

  • 首先函数入栈setTimeout这个函数入栈,当时他需要经过500ms以后才会执行,那么我们的webAPis先去进行计时,那么这是我们其实对setTimeout已经有一个交代了,那么setTimeout出栈,

  • 然后我们就会执行这个while循环

    可能在这个时候,我的定时器已经完成了计时任务,需要执行回调函数了,当时他也是不可以随随便便就可以进入stack里面的,他需要等stack全部都空了,才可以进行

  • 所以,等2秒以后,log结束了,那么才会执行回调cb,然后进行我们熟悉的入栈出栈,得到输出

其实这就是事件循环

事件循环

事件循环:就是查看stackqueue,当stack空的时候,将queue的队头元素放进去

那么我们现在来看点好玩的

setTimeout(callback,0)

我们定义:setTimeout的等待时间是0ms

你可能会疑惑,都0ms了,不使用这个setTimeout不可以嘛?

那么想让我们看一段代码:

console.log("hello");
setTimeout(function cb(){
    console.log("summer")
},0)
console.log("summer瓜瓜")

那么其实他的输出是

hello
summer瓜瓜
summer

我们来模拟一下是如何实现的:

  • 首先,先输出hello
  • 然后我们会遇到一个setTimeout函数,然后,这个会webApis进行计时,那么他会立刻就被变为一个message,进入queue,因为我们栈里面是非空的,所以需要等待。
  • 然后我们继续向下执行输出summer瓜瓜
  • 然后queue将队头拿出来,也就是那个cb回调,然后就会进行入栈和出栈操作
  • 然后输出summer

总结一下:

setTimeout的时间设置为0,其实就是为了让代码到栈顶执行,或者等栈空了,再去执行

总结

事件循环:就是它关注stackqueue,然后将queue的队头放入stack里面

setTimeout:是异步的,他其实是webApis进行处理的,处理好了,将message放入queue里面,当stack空了以后,我们在将message所联系的函数,入栈和出栈

阻塞:其实就是栈里面的函数在进行很慢的操作,导致后面的函数执行不了