JavaScript 是如何异步和单线程的,这个是大多数人知道的一句话,但是理解其真正原理的并不是太多....
简单的来说,JavaScript语言是单线程的,但是他表现出的异步行为并不是语言层面的东西,而是依赖于浏览器内核的,这种异步的特性是通过浏览器API表达出来的。浏览器API内置在Web 浏览器中,它们不是 JavaScript 语言本身的一部分,而是建立在核心 JavaScript 语言之上,可以访问用户机器底层能力的API,比如,访问用户的定位信息,这个实际上背后可能是C 的一些实现,拿到定位数据,返回给到浏览器。
执行栈
执行栈简单的来说,就是执行函数的堆栈,举个例子:
代码语言:javascript复制function main(){
console.log('A');
setTimeout(
function display(){ console.log('B'); }
,0);
console.log('C');
}
main();
// Output
// A
// C
// B
以上函数执行的过程就可以用下面的执行栈来表现出来:
- 首先main函数作为入口函数,肯定是第一个优先放到执行栈中的;
console.log('A')
;是一个函数,虽然用得很多,但是确实是执行一个函数,想console控制台输出文本‘A’,在main函数没执行完时,他就被压入栈中,这很好理解,main函数执行完成必须依赖他的执行。由于console.log('A')
;是一个不依赖任何其他函数的语句,因此,他一上场就被秒杀了。- 随后
setTimeout( function display(){ console.log('B'); } ,0);
被压入栈中,在main的上面,同理,他被执行了,但是他的执行似乎不同,留下了一个尾巴,function display(){ console.log('B'); }
,他被Browser Api
搜集,并且丢到了Message Queue
中去了,但实际上setTimeout
这个函数确实是执行完毕了,因此他退出了栈。 - 随后
console.log('C');
被压入栈执行,不依赖任何函数,因此也被秒杀了 - 然后main函数走到了人生的尽头,此时
执行栈空了!!!
,重要事情说三遍,执行栈空了!!!执行栈空了!!!执行栈空了!!! - 所以,执行栈空了,此时
Message Queue
才有了释放自己收集的任务的机会,此时,function display(){ console.log('B'); }
这个函数里的执行体被压入到执行栈中去执行,其实就是console.log('B')
。
所以 setTimeout(function, delayTime) 中的延迟参数并不代表函数执行后的精确时间延迟。它代表最短等待时间,在此之后的某个时间点将执行该函数,也可以好不怀疑的说,等执行栈空了,他才有机会出现表现自己。
事件循环
所以,事件循环其实就是js代码借助与浏览器API向消息队列中丢入一些回调函数,等待执行栈放空自己的时候,把消息队列中的回调函数压入到执行栈中执行的这么一个机制。
为了证明我的结论所言不虚,我就用一个极端的例子来标明下:
代码语言:javascript复制function main(){
console.log('A');
setTimeout(
function exec(){ console.log('B'); }
, 0);
runWhileLoopForNSeconds(3);
console.log('C');
}
main();
function runWhileLoopForNSeconds(sec){
let start = Date.now(), now = start;
while (now - start < (sec*1000)) {
now = Date.now();
}
}
// Output
// A
// C
// B
- 起初main函数被压入栈中执行,在执行过程中,发现有内容
console.log('A');
- 于是,main得不到退出,
console.log('A');
被压入执行栈中,然后立即执行了,打印了A,紧接着, setTimeout( function exec(){ console.log('B'); } , 0);
被压入执行栈中,setTimeout
函数也被立即执行了,但由于其特殊性,浏览器API将其包裹的回调函数交给了消息队列。因为此时执行中中mian函数还没有执行玩,所以,消息队列里面的消息只能望穿秋水的等待着....- 紧接着runWhileLoopForNSeconds(3);被压入了执行栈中,是一个函数,由于js是单线程的,因此
mian
也好,runWhileLoopForNSeconds
也好,都会在这个执行栈所在在执行上线文中孤独的执行着,不受其他人影响,所以,runWhileLoopForNSeconds
默默的执行了3s,终于执行玩,然后看下main函数执行玩没,还没有,还有 console.log('C');没执行 - 所以,
console.log('C');
被压入了执行栈,然后秒执行了,此时main总算走空了,因此事件循环现在就看消息队列中有没有消息了,已看发现有,嘿,一个一个的丢出来,放到执行栈中来执行。
总结
所以,只有当执行栈中是空的时候,事件循环机制才有机会把消息队列中的任务丢出来执行,换句话说,只有执行栈中有内容在执行,事件循环就不可能给你从消息队列中取任务出来执行。如果英语比较好,可以看看这个视频,一瞬间秒懂 https://www.youtube.com/watch?v=8aGhZQkoFbQ,
附录&参考资料
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/