1. JavaScript是单线程语言
JavaScript是一门单线程语言,在最新的html5中提出了web-worker,但是JavaScript是单线程的核心并没有改变。
所有关于JavaScript的多线程都是由单线程模拟出来的。
2. JavaScript的事件循环
由于JavaScript是单线程,所以很多任务只能一个一个排队,这对于用户体验是非常不友好的。因此聪明的程序员就把任务分为两类:
- 同步任务
-
异步任务
当我们打开一个网页的时候,网页的渲染和dom解析就是同步任务,而加载图片、音乐等大量下载任务的时候就是异步任务。
借用一张图片
- 同步和异步任务分别进入不同的执行“场所”,同步的进入主线程,异步的进入Event Table并注册函数
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue
- 主线程内的任务执行完毕为空的时候,会去Event Queue读取对应的函数,进入主线程执行
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)
如何知道主线程的执行栈为空?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
- ajax进入Event Table,注册回调函数success
- 执行console.log('代码执行结束');
- ajax事件完成,回调函数success进入Event Queue
- 主线程从Event Queue读取回调函数success并执行
3. setTimeout
setTimeout(() => {
task()
},3000)
sleep(10000000)
上述代码是怎么执行的?
- task()进入Event Table并注册,计时开始
- 执行sleep函数, 计时仍在继续
- 3秒到了,计时事件timeout完成,task()进入Event Queue, 但是sleep还没有执行完,只好等待sleep执行完毕
- sleep执行完,task()从Event Queue进入了主线程执行
上述流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务加入Event Queue中,又因为javascript是单线程,任务要一个一个执行,所以如果前面的任务需要的时间太久,那么task任务只能等待线程空闲,导致真正的延时时间远远大于3s。
setTimeout(fn, 0)是否是立即执行呢?答案是不会的。setTimeout(fn, 0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完毕,栈为空就马上执行。
4. setInterval
setInterval会每隔指定的时间将注册的函数置入EventQueue
对于setInterval(fn, ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,就会有fn进入Event Queue。一旦setInterval的回调函数的fn执行时间超出了ms,那么就完全看不出来时间间隔了。
5. Promise和process.nextTick(callback)
process.nextTick(callback)类似node.js的setTimeout,在事件,循环的下一次循环中调用callback函数。
除了广义的同步任务和异步任务,我们对任务的定义有更精细的定义:
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue
事件循环的顺序,决定js的执行顺序。进入整体代码(宏任务)后开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
在一个事件循环中,异步事件返回结果后会被放入一个任务队列中。根据这个异步事件的类型,这个事件实际上会到对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会查看微任务队列中是否有事件存在。如果不存在,那么去宏任务队列中取出一个事件并把对应的回调函数加入到当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的事件,把对应的回调加入当前的执行栈。如此反复循环。
我们只需记住,当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一个事件循环中,微任务永远在宏任务之前执行。
事件循环,宏任务和微任务的关系:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
执行结果为1,7,6,8,2,4,3,5,9,11,10,12
为什么会先执行3,5而不是9,11呢?因为第一个setTimeout宏任务结束之后,会去检查队列中是否有微任务存在,如果有的话先执行微任务。(微任务优先级高)