本篇内容介绍了“JavaScript的并发模型和事件循环机制是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
"单线程"语言
在浏览器实现中,每个单页都是一个独立进程,其中包含了JS引擎、GUI界面渲染、事件触发、定时触发器、异步HTTP请求等多个线程。
进程(Process)是操作系统CPU等资源分配的最小单位,是程序的执行实体,是线程的容器。线程(Thread)是操作系统能够进行运算调度的最小单位,一条线程指的是进程中一个单一顺序的控制流。
因此我们可以说JS是"单线程"式的语言,代码只能按照单一顺序进行串行执行,并在执行完成前阻塞其他代码。
JS数据结构
JS的几种重要数据结构:
● 栈(Stack):用于JS的函数嵌套调用,后进先出,直到栈被清空。
● 堆(Heap):用于存储大块数据的内存区域,如对象。
● 队列(Queue):用于事件循环机制,先进先出,直到队列为空。
事件循环
我们的经验告诉我们JS是可以并发执行的,比如定时任务、并发AJAX请求,那这些是怎么完成的呢?其实这些都是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#L456if (!(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 6671 6692 6703 6724 6765 6816 685
Timers can be nested; 然而,后5个这样的嵌套计时器间隔是被迫至少4毫秒。
特殊场景3:浏览器节流
为了优化后台tab的加载占用资源,浏览器对后台未激活的页面中定时器延迟限制为1s。对追踪型脚本,如谷歌分析等,在当前页面,依然是4ms的延时限制,而后台tabs为10s。
setInterval定时任务
此时,我们会知道,setInterval会在每个定时器延时时间到了后,将一个新的事件fn放入事件队列,如果前面的任务执行太久,我们会看到连续的fn事件被执行而感觉不到时间预设间隔。
因此,我们要尽量避免使用setInterval,改用setTimeout来模拟循环定时任务。