理解javascript中event loop, 了解其背后的原理

问题


今天看到一个问题,什么是 event loop , setTimeoutPromise.resolve().then() 的表现为何又不一样,想了想,决定梳理一下相关知识,这里只说浏览器环境,先不考虑 Node 环境中的 process.nextTick()之类的


先做个简单测试

    console.log(1)
    setTimeout(() => console.log(2), 0)
    new Promise((resolve, reject) => {
        console.log(3)
        resolve()
    }).then(() => {
        console.log(4)
    })

答案是

    1 3 4 2

如果你知道上面的答案,并了解其原理,那你就不用看了,如果你不知道,那就接着看,另外,如果你不了解 Promise, 建议先去看看阮一峰的 promise,了解 promise 最基本的概念

javascript


javascript 是单线程语言,至于其为什么是单线程呢,而不采用多线程。原因就在于,javascipt 是面向用户端的一门语言,其主要作用是与用户交互,渲染数据,操作dom,如果是多线程,就会出现一个问题,比如说,一个线程删除了一个dom节点,另外一个线程添加了一个dom节点,以那个线程为主呢,就会出现混乱的情况。当然,我们可以在操作一个dom之后,加上锁,只允许一个线程操作,但这样,无形之中,程序又平添了复杂程度,未必是一个好的办法。另外,HTML5 中提供了 web workerapi,用来处理例如因大量计算而占用主线程的情况,但按照规定,其也受制于主线程,而且不能操作dom。所以,javascript 是一门单线程语言,也只可能是单线程语言

任务队列


为什么会有任务队列呢,还是因为 javascript 单线程的原因,单线程,就意味着一个任务一个任务的执行,执行完当前任务,执行下一个任务,这样也会遇到一个问题,就比如说,要向服务端通信,加载大量数据,如果是同步执行,js 主线程就得等着这个通信完成,然后才能渲染数据,为了高效率的利用cpu, 就有了 同步任务异步任务 之分。

- 同步任务,进入主线程,一个一个执行
- 异步任务, 进入  `event table ` , 注册回调函数 ` callback `, 任务完成之后,
  将 `callback` 移入  `event queue`, 等待主线程调用

流程图

理解javascript中event loop, 了解其背后的原理_第1张图片

任务队列是一中先进先出的数据结构,排在最前面的优先被主线程读取执行,只要当前执行栈一清空,主线程马上会读取任务队列中的第一任务执行。但当遇到定时器时,需要先检查当前时间是否满足定时器需求,满足执行,不满足自动执行下一个

看一段代码就知道什么意思了

console.log("主线程开始执行任务")
const data = {}
console.log("发现异步任务,执行异步任务,注册回调函数 success 和 fail")
$.ajax({
    method: 'post',
    url: 'https://localhost:8080/user/new',
    data: data,
    success: res => {
        console.log('异步任务执行完成,服务器响应成功,向event queue 推入 success') 
    },
    fail: err => {
        console.log('异步任务执行,服务器响应失败,向event queue 推入 fail')  
    }
})

console.log("同步任务执行完成,主线程检查 event queue ")
console.log("ajax 请求成功响应, 检测到 event queue 中的 success ,执行success")

上面的代码,我已经对 event loop 有了初步了解,那么接下来梳理一下,同样是异步任务,为何不同类型的异步任务表现却不一样

setTimeout

setTimeout 想必每个人都很熟悉,一次性定时器,我们一般都这么用它

    setTimeout(() => {
        console.log("五秒了,我要执行了")
    }, 5000)

一般情况下,我们设置一个定时器,如果不去特意的测试,是发现不了,定时器时间并不准确这个问题的,我们做个测试

    console.time('timer')
    setTimeout(() => console.timeEnd('timer'), 5000)
    sleep(1000000) // 或者执行一大堆复杂的逻辑

我们这个时候发现,输出的时间,并不是 精确的 5000 ,而是根据你后面同步任务执行的时间会有所影响

我们来分析一下,为什么

- 主线程执行console.time('timer'), 开始及时
- 执行到setTimeout(), 进入到 `event table` 中并注册回调函数
- 开始执行sleep(), sleep 执行的很慢,这个时候,5 秒到了,定时器时间到了之后向
  `event queue` 中推送了回调函数,但主线程一直在忙,没有时间去检查 
  `event queue`, 一直等到 `sleep()` 执行完成,主线程才检查 `event queue`,
  并执行回调函数

上述的流程走完,我们知道 setTimeout 这个函数,是经过指定时间后,把要执行的任务加入到 event queue 中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。

那么,我们还会遇到一种情况

    setTimeout(fn, 0)

那么这种情况,假设主线程并不繁忙,那么 fn 一定就会在 不延迟执行吗,并不会,
即便主线程为空,0ms 实际上也是达不到的。根据HTML的标准,最低是 4ms。所以不可能做到 0ms

setInterval

前面说了setTimeout, 那么 setInterval 自然也要说一下。 setInterval 的表现和 setTimeout 区别不大,区别之处在于,setInterval 是重复执行,每隔 ms 秒之后,将已经注册好的回调函数推入 event queue 中,等待主线程调用。同样,如果主线程繁忙,那回调函数的执行自然也就会有延迟

promise

回到最开始的例子

    console.log(1)
    setTimeout(() => console.log(2), 0)
    new Promise((resolve, reject) => {
        console.log(3)
        resolve()
    }).then(() => {
        console.log(4)
    })

如果同样按照异步任务的先后执行顺序去考虑的话,答案应该是 1 3 2 4, 但为什么会是 1 3 4 2 呢, 这里就要说到 微任务宏任务 了, 除了广义的 同步任务异步任务 之分,对 异步任务 还有更细致的区分,就是 micro Task (微任务)macro Task (宏任务)

  • micor Task 包括 promise, 当然还有 Node 环境中的,但这里先不梳理

  • macro Task 包括 scriptsetTimeoutsetInterval

不同类型的任务,会进入不同的 event queue, 同是相同类型的任务,进入相同的 event queue, 例如: setTimeoutsetInterval 会进入相同的 event queue

再来分析一下第一个例子


                    
                    

你可能感兴趣的:(javascript,event,loop,nextTick)