因为JavaScript是一门单线程语言,所以我们可以得出结论:
- JavaScript是按照语句出现的顺序执行的
let a = '1';
console.log(a);
let b = '2';
console.log(b);
然而实际上js是这样的:
setTimeout(function () {
console.log('定时器开始啦!')
})
new Promise(function (resolve) {
console.log('for循环开始啦!')
for (var i = 0; i < 10000; i ++) {
i == 99 && resolve()
}
}).then(function () {
console.log('执行then函数')
})
console.log('代码执行结束')
// chrome 验证下顺序
// for循环开始啦!
// 代码执行结束
// 执行then函数
// 定时器开始啦!
1. 关于JavaScript
JavaScript是一门单线程语言,在html5中提出来web-worker,但javascript是单线程这一核心未改变,所以一切JavaScript的多线程都是单线程模拟出来的
2.JavaScript事件循环
分为两类:
- 同步任务
-
异步任务
15fdd88994142347.jpg
图片用文字来表达的话:
- 同步和异步任务分别进入不同的执行场所,同步任务进入主线程,异步任务进入Event Table并注册函数。
- 当指定的事情完成后,Event Table 会将这个函数移入Event Queue。
- 主线程内的任务执行完,会去Event Queue读取对应得函数,进入主线程执行。
- 上述的过程会不断重复,也就是常说的Event Loop(事件循环)
那么怎么知道主线程执行栈为空?js引擎在monitoring process进程,会持续不断的和检查主线程执行栈是否为空,一旦为空,回去Event Queue检查是否有等待被调用的函数。
上一段代码:
let data = []
$.ajax({
url: 'www.js.com',
data: data,
success: function () {
console.log('success')
}
})
console.log('代码执行结束')
- ajax进入Event Table,注册回调函数success。
- 执行console.log('代码执行结束')
- ajax事件完成,回调函数进入Event Queue。
- 主线程完成所有任务开始读取Event Queue的回调函数success并执行
3.setTimeout
大名鼎鼎的setTimeout无需再多言,大家对他的第一印象就是异步可以延时执行,我们经常这么实现延时3秒执行:
setTimeout(() => {
console.log('延时3秒');
},3000)
渐渐的setTimeout用的地方多了,问题也出现了,有时候明明写的延时3秒,实际却5,6秒才执行函数,这又咋回事啊?
先看一个例子:
setTimeout(() => {
task();
},3000)
console.log('执行console');
根据前面我们的结论,setTimeout是异步的,应该先执行console.log这个同步任务,所以我们的结论是:
//执行console
//task()
去验证一下,结果正确!
然后我们修改一下前面的代码:
setTimeout(() => {
task()
},3000)
sleep(10000000)
乍一看其实差不多嘛,但我们把这段代码在chrome执行一下,却发现控制台执行task()需要的时间远远超过3秒,说好的延时三秒,为啥现在需要这么长时间啊?
这时候我们需要重新理解setTimeout的定义。我们先说上述代码是怎么执行的
- task()进入Event Table 并注册,计时开始。
- 执行sleep函数,很慢,计时仍在继续。
- 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep还没有执行完成。
- sleep执行完成了,task()终于从Event Queue进入主线程开始执行。
上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。
关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。有兴趣的同学可以自行了解。
4.setInterval
上面说完了setTimeout,当然不能错过它的孪生兄弟setInterval。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。这句话请读者仔细品味。
5.Promise与process.nextTick(callback)
Promise的定义和功能本文不再赘述,不了解的读者可以学习一下阮一峰老师的Promise。而process.nextTick(callback)类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。
进入正题:除了广义上的同步任务和异步任务,我们对任务有更精细的定义
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task (微任务):promise, process.nextTick
不同的任务类型会进入对应得Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用文章最开始的一段代码说明:
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 这段代码作为宏任务,进入主线程。
- 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。
- 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
- 遇到console.log,立即执行
- 整体代码的script作为第一个宏任务执行结束,接下来查看微任务,发现then在微任务Event Queue中,执行。
- 第一轮宏任务和微任务执行结束,开始第二轮任务,在Event Queue中发现了setTimeout对应得回调函数,执行。
- 结束。
