今天看到一个问题,什么是 event loop
, setTimeout
和 Promise.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
是单线程语言,至于其为什么是单线程呢,而不采用多线程。原因就在于,javascipt
是面向用户端的一门语言,其主要作用是与用户交互,渲染数据,操作dom
,如果是多线程,就会出现一个问题,比如说,一个线程删除了一个dom
节点,另外一个线程添加了一个dom
节点,以那个线程为主呢,就会出现混乱的情况。当然,我们可以在操作一个dom之后,加上锁,只允许一个线程操作,但这样,无形之中,程序又平添了复杂程度,未必是一个好的办法。另外,HTML5
中提供了 web worker
等 api
,用来处理例如因大量计算而占用主线程的情况,但按照规定,其也受制于主线程,而且不能操作dom。所以,javascript
是一门单线程语言,也只可能是单线程语言
为什么会有任务队列呢,还是因为 javascript
单线程的原因,单线程,就意味着一个任务一个任务的执行,执行完当前任务,执行下一个任务,这样也会遇到一个问题,就比如说,要向服务端通信,加载大量数据,如果是同步执行,js
主线程就得等着这个通信完成,然后才能渲染数据,为了高效率的利用cpu
, 就有了 同步任务
和 异步任务
之分。
- 同步任务,进入主线程,一个一个执行
- 异步任务, 进入 `event table ` , 注册回调函数 ` callback `, 任务完成之后,
将 `callback` 移入 `event queue`, 等待主线程调用
流程图
任务队列是一中先进先出的数据结构,排在最前面的优先被主线程读取执行,只要当前执行栈一清空,主线程马上会读取任务队列中的第一任务执行。但当遇到定时器时,需要先检查当前时间是否满足定时器需求,满足执行,不满足自动执行下一个
看一段代码就知道什么意思了
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(() => {
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
前面说了setTimeout
, 那么 setInterval
自然也要说一下。 setInterval
的表现和 setTimeout
区别不大,区别之处在于,setInterval
是重复执行,每隔 ms
秒之后,将已经注册好的回调函数推入 event queue
中,等待主线程调用。同样,如果主线程繁忙,那回调函数的执行自然也就会有延迟
回到最开始的例子
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
包括 script
、setTimeout
、setInterval
不同类型的任务,会进入不同的 event queue
, 同是相同类型的任务,进入相同的 event queue
, 例如: setTimeout
和 setInterval
会进入相同的 event queue
再来分析一下第一个例子