本文介绍了“什么是JavaScript的并发模型和事件循环机制”的相关知识。很多人在实际案件操作中都会遇到这样的困难。接下来,让边肖带领大家学习如何应对这些情况!希望大家认真阅读,学点东西!
"单线程"语言
在浏览器实现中,每一个单独的页面都是一个独立的进程,包括JS引擎、GUI界面渲染、事件触发、定时触发、异步HTTP请求等线程。
进程是操作系统的CPU、程序的执行实体、线程的容器等资源分配的最小单位。线程是操作系统能够调度操作的最小单元,线程指的是进程中的单阶控制流。
因此,我们可以说JS是一种‘单线程’语言。代码只能以单个顺序连续执行,其他代码在执行完成之前可以被阻止。
JS数据结构
JS的几个重要数据结构:
栈:对JS、LIFO的函数进行嵌套调用,直到栈被清空。
堆:用于存储大块数据(如对象)的内存区域。
队列:用于事件循环机制,FIFO直到队列为空。
事件循环
我们的经验告诉我们,JS可以并发执行,比如定时任务和并发的AJAX请求。这些是怎么做的?其实这些都是JS用单线程模拟多线程来完成的。
如上图所示,JS串行执行主线程任务。当遇到异步任务(如计时器)时,它会被放入事件队列。主线程任务完成后,遍历事件队列取出组长任务执行,直到队列为空。
所有执行完成后,会有一个主监控进程,它会持续检查队列是否为空,如果不是,则继续事件循环。
setTimeout定时任务
定时任务setTimeout(fn,Timeout)将首先移交给浏览器的定时器模块。当延迟时间到了,事件将被放入事件队列。主线程执行后,如果队列中没有其他任务,会立即处理。如果有尚未完成的任务,则在完成所有之前的任务之前,不会执行该任务。因此,setTimeout的第二个参数是最小延迟时间,而不是等待时间。
当我们预计一个操作将是繁重和耗时的,并且我们不想阻塞主线程的执行时,我们将使用立即执行任务:
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,但是
//https://github.com/nodejs/node/blob/v 8 . 9 . 4/lib/timers . js # L456 if(!(after=1 after=time out _ MAX))after=1;//scheduleonexttick,FollowBrowser行为浏览器使用32位存储延迟。如果大于2.32-1毫秒(24.8天),溢出将立即执行。
特殊场景2:最小延迟为4ms
当定时器嵌套调用超过4层时,最小间隔为4毫秒;
vari=0;functioncb(){console.log(i,newDate()。get毫秒());if(i20)setTimeout(cb,0);
nbsp; i++;}setTimeout(cb, 0);
可以看到前4层也不是标准的立刻执行,在第4层后间隔明显变大到4ms以上:
0 6671 6692 6703 6724 6765 6816 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()函数。
宏任务和微任务都有自身的事件循环机制,也拥有独立的事件队列(Event Queue),都会按照队列的顺序依次执行。但宏任务和微任务主要有两点区别:
1、宏任务执行完成,在控制权交还给主线程执行其他宏任务之前,会将微任务队列中的所有任务执行完成。
2、微任务创建的新的微任务,会在下一个宏任务执行之前被继续遍历执行,直到微任务队列为空。
浏览器的进程和线程
浏览器是多进程式的,每个页面和插件都是一个独立的进程,这样可以保证单页面崩溃或者插件崩溃不会影响到其他页面和浏览器整体的稳定运行。
它主要包括:
1、主进程:负责浏览器界面显示和管理,如前进、后退,新增、关闭,网络资源的下载和管理。
2、第三方插件进程:当启用插件时,每个插件独立一个进程。
3、GPU进程:全局唯一,用于3D图形绘制。
4、Renderer渲染进程:每个页面一个进程,互不影响,执行事件处理、脚本执行、页面渲染。
单页面线程
浏览器的单个页面就是一个进程,指的就是Renderer进程,而进程中又包含有多个线程用于处理不同的任务,主要包括:
1、GUI渲染线程:负责HTML和CSS的构建成DOM树,渲染页面,比如重绘。
2、JS引擎线程:JS内核,如Chrome的V8引擎,负责解析执行JS代码。
3、事件触发线程:如点击等事件存在绑定回调时,触发后会被放入宏任务事件队列。
4、定时触发器线程:setTimeout和setInterval的定时计数器,在时间到达后放入宏任务事件队列。
5、异步HTTP请求线程:XMLHTTPRequest请求后新开一个线程,等待状态改变后,如果存在回调函数,就将其放入宏任务队列。
需要注意的是,GUI渲染进程和JS引擎进程互斥,两者只会同时执行一个。主要的原因是为了节流,因为JS的执行会可能多次改变页面,页面的改变也会多次调用JS,如resize。因此浏览器采用的策略是交替执行,每个宏任务执行完成后,执行GUI渲染,然后执行下一个宏任务。
Webworker线程
因为JS只有一个引擎线程,同时和GUI渲染线程互斥,因此在繁重任务执行时会导致页面卡住,所以在HTML5中支持了Webworker,它用于向浏览器申请一个新的子线程执行任务,并通过postMessage API来和worker线程通信。所以我们在繁重任务执行时,可以选择新开一个Worker线程来执行,并在执行结束后通信给主线程,这样不会影响页面的正常渲染和使用。
总结
1、JS是单线程、阻塞式执行语言。
2、JS通过事件循环机制来完成异步任务并发执行。
3、JS将任务细分为宏任务和微任务来提供执行优先级。
4、浏览器单页面为一个进程,包含的JS引擎线程和GUI渲染线程互斥,可以通过新开Web Worker线程来完成繁重的计算任务。
最后给大家出一个考题,可以猜下执行的输出结果来验证学习成果:
function sleep(ms) { console.log('before first microtask init'); new Promise(resolve => { console.log('first microtask'); resolve() }) .then(() => {console.log('finish first microtask')}); console.log('after first microtask init'); return new Promise(resolve => { console.log('second microtask'); setTimeout(resolve, ms); });}setTimeout(async () => { console.log('start task'); await sleep(3000); console.log('end task');}, 0);setTimeout(() => console.log('add event'), 0);console.log('main thread');
输出为:
main threadstart taskbefore first microtask initfirst microtaskafter first microtask initsecond microtaskfinish first microtaskadd eventend task
“JavaScript的并发模型和事件循环机制是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/107416.html