面试之万能答案:事件循环

2,717 阅读7分钟

面试时你是否会遇到下面的问题?

  1. setTimeout为什么没有按写好的延迟时间执行?
  2. setTimeout的实现原理是什么?
  3. 你知道Event Loop吗?
  4. 你能说下js的异步执行机制吗?
  5. 你能说下宏任务和微任务吗?
  6. 你能写出下面的代码的最终的执行结果吗?
console.log(1)
setTimeout(function(){
	console.log(2)
},1000)
new Promise(function(resolve,rejected){
	console.log(3)
	resolve('同步任务结果')
}).then(res=>{
	console.log('4',res)
})
console.log(5)

其实这些问题的核心都是一个:事件循环,那事件循环到底是什么呢?

那下面就让我们来一起学起来吧💃💃

一、事件循环

大家都知道js是个单线程,单线程就意味着任务要排队,那么像setTimeout,ajax这种任务,如果也要排队,就会造成等待耗时,长时间等待如果无响应,就会造成页面空白,用户体验差。所以就出现了同步异步任务,同步任务在主线程上执行,异步任务在旁边执行,执行后把结果放在任务队列中,等到同步任务全都执行完毕后,就去任务队列中取异步任务的结果,然后按顺序执行。异步任务队列中还分为微任务和宏任务,微任务先与宏任务执行。如此反复的一个过程就叫事件循环,也就是js的异步执行机制

下面来逐条分析

1.首先js是单线程,同一时间只能做一件事情

为什么是单线程呢?因为js多用来处理dom操作,如果是多线程,2个线程分别对dom做操作, 浏览器不能知道以哪个线程为主,所以js必须是单线程

2. 单线程也就意味着所有的任务是要排队的

浏览器虽然是多线程的,但是只分配了一个主线程给js执行任务,而且一次只能执行一个任务, 但是js中很多的比如
网络请求(http请求的连接需要tcp三次握手建立连接,有等待时间)
定时器(延时操作)
事件监听(onclick事件)
等会非常的耗时,导致执行效率低下,甚至页面假死

3. 所以浏览器为异步的任务开启了另外的线程,那个异步任务的线程完成任务后,主线程是怎么知道的呢?(异步叫任务队列)

整个程序是事件驱动的,每个事件都会绑定相应的回调函数,任务队列中都是已经完成的异步操作, 而不是你注册的异步事件 注意:无论异步操作何时开始执行,只要执行完毕就放在消息队列中,也就是说放在队列中的是回调函数的结果

4. 任务队列又分为

1.宏任务:

  • script脚本(整体代码)、
  • setTimeout、setInterval、
  • I/O(点击按钮之类的交互)、
  • postMessage、
  • MessageChannel:允许我们创建一个新的消息通道
  • setImmediate(Node.js环境):把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成后面的其他语句后, 就立即执行这个回调函数

Vue 中对于宏任务的实现:

  • 优先检测是否支持原生的setImmediate(这是一个高版本IE和Edge才支持的特性)
  • 再去检测是否原生支持MessageChannel
  • 以上都不支持, 就降级为setTimeout 0

2.微任务:

  • ES6 的Promise:链式操作,解决js的地狱回调问题
  • MutaionObserver:可以监听dom结构变化的接口
  • process.nextTick(Node.js环境):定义一个动作,并且让这个动作在下一个事件轮询的时间点上执行

他们的执行顺序是:先执行所有的微任务,再执行下一个宏任务

所有进入异步的都是事件回调的那部分代码,所以

new promise实例化过程中的代码是同步的,then之后执行的才是微任务,微任务会在宏任务之前执行

await 是基于promise封装的,所以await之前的是同步代码,之后的是异步

5. 所以js异步的执行机制就是事件循环机制

  1. 所有的任务都在主线程执行, 形成一个执行栈
  2. 主线程之外存在一个任务队列(task queue),只要异步任务有了运行结果, 就会放置在任务队列中等待
  3. 当同步任务全部完成后就会去任务队列中读取异步任务,进入执行栈执行
  4. 主线程不断的重复上面三个步骤,所以叫事件循环机制

6.答案

看完上面应该就知道该如何回答面试官的问题了吧!

  • 1:因为事件循环是先执行同步任务,再执行异步任务,而setTimeout属于异步任务,如果延时时间是1秒,但是同步任务太多,都完成需要3秒,那么自然就没有按照延迟的时间1秒执行了。
  • 2:setTimeout的实现原理就是事件循环:将setTimeout的处理结果放在异步任务队列中,等到执行栈中的任务完成后,按放入顺序去队伍队列读取任务
  • 3、4:都是一个答案,就是上面总结的那一段,简单说就是其实就是主线程不断的去任务队列读取事件,就叫事件循环机制
  • 5:异步任务分为宏任务和微任务,当要执行一个宏任务时,先去看看微任务队列中是否有微任务,然后先执行完所有的微任务,再去执行宏任务
  • 6:1 3 5 4 2

1 3 5 是同步任务。按顺序执行。
4 是微任务。
2 是宏任务。

7.看个著名的图片巩固一下

2.png
在上图中,调用栈中遇到DOM操作、ajax请求以及setTimeout等WebAPIs的时候就会交给浏览器内核的其他模块进行处理,webkit内核在Javasctipt执行引擎之外,有一个重要的模块是webcore模块。.对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现。等到这些模块处理完这些操作的时候将回调函数放入任务队列中,之后等栈中的task执行完之后再去执行任务队列之中的回调函数。

二、setTimeout的应用

既然已经说到这里了, 顺便说下有关于setTimeout的应用的面试题

1. 你知道函数的防抖节流吗?可以写一下实现代码吗?

  • 防抖:就是延时操作
  • 应用场景:input框搜索,多次输入操作最终作为一次操作
<!--简单版本-->
var timer ;
function debounce(){
    if(timer){
        clearTimeout(timer)
    }
    timer = setTimeout(()=>{
        console.log("一秒后发请求")
    },1000)
}
debounce();
  • 节流:固定时间内只执行一次
  • 应用场景:btn提交,多次点击只提交一次,需要一个标识,为false时不提交。
<!--简单版本-->
var flag = true;
function throttle() {
    if (!flag) return false;
    flag = false;
    setTimeout(() => {
        console.log('2秒内如论调用多少次此函数,都只会打印一次结果')
        flag = true;
    }, 2000)
}
throttle();

传参数版

2. 能写一下SetTimeout 实现setInterval吗?

setTimout 延迟操作,setInterval是按指定周期调用函数 所以核心应该就是,

  • 递归调用setTimeout
  • 有个timeid是全局变量
  • 加入清除功能的思路是这样的:给setTimeout一个id,然后用clearTimeout(id)清除,

如果timeid写在了函数里,那么你去控制台调用一下代码会发现,最终return的timeid是210 ,但是每次递归调用的setTimeout的id却都是不一样的,然而最后return出来的并不是最新的id,那么你去清除210的时候就发现程序依然在跑。所以timeid需要是全局变量

//简单版本
let timeid;
function myInterval(fn,time){
    function inner(){
        fn();
        timeid = setTimeout(inner,time)
    }
    inner();
    return timeid;
}
myInterval(()=>{console.log('重复调用')},1000)
setTimeout(()=>{
    clearTimeout (timeid)
},6000)