作者:我叫叮当既小号 | 来源:互联网 | 2020-09-02 21:14
我们知道JS语言是串行执行、阻塞式、事件驱动的,那么它又是怎么支持并发处理数据的呢?
如上图所示,JS串行执行主线程任务,当遇到异步任务如定时器时,将其放入事件队列中,在主线程任务执行完毕后,再去事件队列中遍历取出队首任务进行执行,直至队列为空。
全部执行完成后,会有主监控进程,持续检测队列是否为空,如果不为空,则继续事件循环。
setTimeout定时任务
定时任务setTimeout(fn, timeout)
会先被交给浏览器的定时器模块,等延迟时间到了,再将事件放入到事件队列里,等主线程执行结束后,如果队列中没有其他任务,则会被立即处理,而如果还有没有执行完成的任务,则需要等前面的任务都执行完成才会被执行。因此setTimeout的第2个参数是最少延迟时间,而非等待时间。
当我们预期到一个操作会很繁重耗时又不想阻塞主线程的执行时,会使用立即执行任务:
setTimeout(fn, 0);
特殊场景1:最小延迟为1ms
然而考虑这么一段代码会怎么执行:
setTimeout(()=>{console.log(5)},5)
setTimeout(()=>{console.log(4)},4)
setTimeout(()=>{console.log(3)},3)
setTimeout(()=>{console.log(2)},2)
setTimeout(()=>{console.log(1)},1)
setTimeout(()=>{console.log(0)},0)
了解完事件队列机制,你的答案应该是0,1,2,3,4,5
,然而答案却是1,0,2,3,4,5
,这个是因为浏览器的实现机制是最小间隔为1ms。
// https://github.com/nodejs/node/blob/v8.9.4/lib/timers.js#L456
if (!(after >= 1 && after <= TIMEOUT_MAX))
after = 1; // schedule on next tick, follows browser behavior
浏览器以32位bit来存储延时,如果大于 2^32-1 ms(24.8天)
,导致溢出会立刻执行。
特殊场景2:最小延迟为4ms
定时器的嵌套调用超过4层时,会导致最小间隔为4ms:
var i=0;
function cb() {
console.log(i, new Date().getMilliseconds());
if (i <20) setTimeout(cb, 0);
i++;
}
setTimeout(cb, 0);
可以看到前4层也不是标准的立刻执行,在第4层后间隔明显变大到4ms以上:
0 667
1 669
2 670
3 672
4 676
5 681
6 685
Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.
特殊场景3:浏览器节流
为了优化后台tab的加载占用资源,浏览器对后台未激活的页面中定时器延迟限制为1s。
对追踪型脚本,如谷歌分析等,在当前页面,依然是4ms的延时限制,而后台tabs为10s。
setInterval定时任务
此时,我们会知道,setInterval会在每个定时器延时时间到了后,将一个新的事件fn放入事件队列,如果前面的任务执行太久,我们会看到连续的fn事件被执行而感觉不到时间预设间隔。
因此,我们要尽量避免使用setInterval,改用setTimeout来模拟循环定时任务。
睡眠函数
JS一直缺少休眠的语法,借助ES6新的语法,我们可以模拟这个功能,但是同样的这个方法因为借助了setTimeout也不能保证准确的睡眠延时:
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
})
}
// 使用
async function test() {
await sleep(3000);
}
async await机制
async函数是Generator函数的语法糖,提供更方便的调用和语义,上面的使用可以替换为:
function* test() {
yield sleep(3000);
}
// 使用
var g = test();
test.next();
但是调用使用更加复杂,因此一般我们使用async函数即可。但JS时如何实现睡眠函数的呢,其实就是提供一种执行时的中间状态暂停,然后将控制权移交出去,等控制权再次交回时,从上次的断点处继续执行。因此营造了一种睡眠的假象,其实JS主线程还可以在执行其他的任务。
Generator函数调用后会返回一个内部指针,指向多个异步任务的暂停点,当调用next函数时,从上一个暂停点开始执行。
协程
协程(coroutine)是指多个线程互相协作,完成异步任务的一种多任务异步执行的解决方案。他的运行流程:
● 协程A开始执行
● 协程A执行到一半,进入暂停,执行权转移到协程B
● 协程B在执行一段时间后,将执行权交换给A
● 协程A恢复执行
可以看到这也就是Generator函数的实现方案。
宏任务和微任务
一个JS的任务可以定义为:在标准执行机制中,即将被调度执行的所有代码块。
我们上面介绍了JS如何使用单线程完成异步多任务调用,但我们知道JS的异步任务分很多种,如setTimeout定时器、Promise异步回调任务等,它们的执行优先级又一样吗?
答案是不。JS在异步任务上有更细致的划分,它分为两种:
宏任务(macrotask)包含:
● 执行的一段JS代码块,如控制台、script元素中包含的内容。
● 事件绑定的回调函数,如点击事件。
● 定时器创建的回调,如setTimeout和setInterval。
微任务(microtask)包含:
● Promise对象的thenable函数。
● Nodejs中的process.nextTick函数。
● JS专用的queueMicrotask()函数。
最后给大家出一个考题,可以猜下执行的输出结果来验证学习成果:
function sleep(ms) {
console.log(&#39;before first microtask init&#39;);
new Promise(resolve => {
console.log(&#39;first microtask&#39;);
resolve()
})
.then(() => {console.log(&#39;finish first microtask&#39;)});
console.log(&#39;after first microtask init&#39;);
return new Promise(resolve => {
console.log(&#39;second microtask&#39;);
setTimeout(resolve, ms);
});
}
setTimeout(async () => {
console.log(&#39;start task&#39;);
await sleep(3000);
console.log(&#39;end task&#39;);
}, 0);
setTimeout(() => console.log(&#39;add event&#39;), 0);
console.log(&#39;main thread&#39;);
输出为:
main thread
start task
before first microtask init
first microtask
after first microtask init
second microtask
finish first microtask
add event
end task
本文来自 js教程 栏目,欢迎学习!
以上就是深入理解Javascript的并发模型和事件循环机制的详细内容,更多请关注 第一PHP社区 其它相关文章!