JavaScript是一门单线程语言 一切JavaScript中说的多线程都是纸老虎 本质都是用单线程模拟出来的
先来一张来自大神(mr.z 大佬)的图
本文的目的就是彻底弄懂JavaScript的事件循环的执行机制。
首先,不论你是面试求职,还是日常的开发工作,给定的几行代码,我们需要准确的知道代码执行输出的结果,也就是代码执行的顺序。因为JavaScript是单线程语言,所以它的语句都是顺序执行的,也就是同步任务。但是,如果加载一个包含了很多图片的页面,如果都是同步任务,要是图片资源巨大加载不出来,岂不是后面的内容都显示不了。所以就要了解JavaScript的事件执行机制。
其实,当打开页面时,页面的渲染过程涉及到很多同步任务(页面骨架,页面元素的渲染),也有一些异步任务(图片音乐之类占用资源大耗时比较久的任务)。
同步任务和异步任务会在不同的执行场所等待被执行,同步任务会在主线程中执行,碰到异步任务会先放到异步队列中等待被执行。
AJAX(Asynchronous Javascript And XML)异步JavaScript和XML
,相信我们在日常工作中ajax
已经用过很多了,通过和后端进行少量数据交互,实现网页异步更新。
ajax
出现之前,如果网页需要更新部分数据,比如我们获取一个表单数据,就必须重载整个网页页面。
有了ajax
技术,我们就可以在不重载整个网页的情况下,对网页的某部分进行更新,比如只更新表单数据,而不用对表单整个页面进行重载。
看一个例子:
$.ajax({
url: 'xxx.com/list',
data: xxx,
success: () => {
console.log('接收数据了')
}
})
console.log('代码执行结束')
这一段简单的ajax
请求代码就是个简单的异步任务:
ajax
进入事件列表,注册回调函数success
console.log('代码执行结束')
ajax
事件完成,回调函数success
进入事件队列(Event Queue)success
并执行setTimeout
也是异步任务。那到底是如何异步执行的呢?
通常我们延时执行时会想到用setTimeout
setTimeout(() => {
console.log('延时1s')
}, 1000)
逐渐用的多了,问题也多了,有时候延时1秒但是实际却过了3,4秒才执行。比如下面的例子
setTimeout(() => {
task()
}, 1000)
console.log('主流程console')
sleep(100000) // 执行了很久的函数
我们知道setTimeout
是个异步任务,这里我们想要task
在1秒后执行,但是实际等待的时间却远远不止1秒,这是为什么?我们弄懂这里的setTimeout
异步任务是如何执行的就明白了:
setTimeout
,发现是个异步任务时,会注册其回调函数task
console
sleep
函数,执行了很久很久…task
进入到事件队列中(如果这时候主线程任务都执行完了是空闲的,下一步主线程就会从事件队列中取到这个函数去主线程中执行了)。此时应该开始执行事件队列中注册的task
了,但是主线程还在执行sleep
,因为js是单线程,所以只能继续等待sleep
终于执行完了,task
从事件队列中取出进入到主线程中执行上述流程了解完,我们就知道setTimeout
的延时时间,是指定经过多长时间把要执行的任务加入到事件队列中去,所以真实等待的时长就不仅仅是根据setTimeout
的延时时间参数来决定的了,还依赖主线程中任务执行的时长。
setInterval
跟setTimeout
很像,它的含义是指定多长时间将注册的函数放入事件队列中,如果前面的任务耗时很长,同样也会延长这个时间。通过对setTimeout
的说明,知道对setInterval(fn, ms)
的理解需要注意的是:不是每隔ms
执行一次fn
,而是每隔ms
,会将fn
加入到事件队列中一次。
所以,细品这句话:如果fn
执行时间超过了ms
,用户是看不出这个时间间隔的。因为,如果你每隔1秒加入一个fn
,而这个fn
每次执行都需要5秒才能结束,也就是主线程第一个执行完的时候去查队列时,发现队列中还有4个没执行呢,此时主线程就又会去队列中拿一个新的fn
到主线程中去执行,也就没有什么休息时间了。
ES6
为我们提供了Promise
来处理异步操作。
先看一个例子:
setTimeout(() => {
console.log('setTimeout')
})
new Promise((resolve) => {
console.log('promise')
resolve()
}).then(() => {
console.log('then')
})
console.log('console')
根据之前的介绍,如果promise
和setTimeout
的事件循环机制相同的话,结果应该是
// promise
// console
// setTimeout
// then
但是正确的执行结果却是:
// promise
// console
// then
// setTimeout
所以我们知道,虽然都是异步任务,但是promise
和setTimeout
却是不同的异步任务,异步任务有两种
script
,setTimeout
,setInterval
,UI渲染,I/O
,postMessage
等)promise
,process.nextTick
)不同的任务也会进入不同的队列中,循环任务执行时也会不同。上述的例子具体的执行步骤:
script
中,所以首先整段代码作为一个宏任务,进入主线程setTimeout
,此时将其回调函数注册后发布到事件队列中promise
,因为我们知道promise
是立即执行的(可参考Promise原理和使用),而then
回调会分发到微任务队列中console
then
,执行setTimeout
的回调,执行。所以,宏微任务的关系如下:
最后一个复杂的例子,检查你是否掌握了这个执行机制:
console.log(1)
setTimeout(() => {
// setTimeout_1
console.log(2)
new Promise((resolve) => {
console.log(3)
resolve()
}).then(() => {
// promise_3
console.log(4)
})
}, 1000)
new Promise((resolve) => {
console.log(5)
resolve()
}).then(() => {
// promise_1
setTimeout(() => {
// setTimeout_3
console.log(6)
})
console.log(7)
})
setTimeout(() => {
// setTimeout_2
console.log(8)
new Promise((resolve) => {
console.log(9)
resolve()
}).then(() => {
// promise_2
console.log(10)
})
})
第一轮事件循环:
script
宏任务,遇到console.log(1)
setTimeout
回调1秒后放入宏事件队列,记为setTimeout_1
promise
立即执行,console.log(5)
同时then
放入微任务中,记为promise_1
setTimeout
,立即放入宏任务队列中,记为setTimeout_2
promise_1
,执行时候发现又遇到一个宏任务,将其放入宏任务队列中,记为setTimeout_3
,继续执行console.log(7)
setTimeout_1
也被加入到宏队列中了1,5,7
,此时任务队列宏任务 |
---|
setTimeout_2 |
setTimeout_3 |
setTimeout_1 |
第二轮事件循环:
setTimeout_2
,执行console.log(8)
,立即执行promise
的console.log(9)
,同时将其then
加入到微任务中,记为promise_2
promise_2
,执行console.log(10)
1,5,7,8,9,10
第三轮事件循环:
setTimeout_3
,console.log(6)
,无微任务,结束1,5,7,8,9,10,6
第四轮事件循环:
setTimeout_1
立即执行console.log(2)
,遇到promise
立即执行console.log(3)
,同时将then
加入到微任务中,记为promise_3
promise_3
,执行console.log(4)
1,5,7,8,9,10,6,2,3,4
从头到尾都在强调JavaScript是一门单线程语言,不管是什么新框架还是新语法糖实现的所谓的异步,其实都是根据JavaScript的事件循环机制原理用同步的方式来模拟的,事件循环是JavaScript实现异步的一种方法,也是它的执行机制。
牢牢记住这两点:
new Promise
或者定时器是同步立即执行的,异步的是他们的then
和回调函数方法