搜索
首页web前端js教程深入理解JavaScript的并发模型和事件循环机制

深入理解JavaScript的并发模型和事件循环机制

Nov 27, 2019 pm 04:08 PM
javascriptsettimeout协程线程

我们知道JS语言是串行执行、阻塞式、事件驱动的,那么它又是怎么支持并发处理数据的呢?

深入理解JavaScript的并发模型和事件循环机制

"单线程"语言

在浏览器实现中,每个单页都是一个独立进程,其中包含了JS引擎、GUI界面渲染、事件触发、定时触发器、异步HTTP请求等多个线程。

进程(Process)是操作系统CPU等资源分配的最小单位,是程序的执行实体,是线程的容器。
线程(Thread)是操作系统能够进行运算调度的最小单位,一条线程指的是进程中一个单一顺序的控制流。

因此我们可以说JS是"单线程"式的语言,代码只能按照单一顺序进行串行执行,并在执行完成前阻塞其他代码。

【相关课程推荐:JavaScript视频教程

JS数据结构

1.png

如上图所示为JS的几种重要数据结构:

 ● 栈(Stack):用于JS的函数嵌套调用,后进先出,直到栈被清空。

 ● 堆(Heap):用于存储大块数据的内存区域,如对象。

 ● 队列(Queue):用于事件循环机制,先进先出,直到队列为空。

事件循环

我们的经验告诉我们JS是可以并发执行的,比如定时任务、并发AJAX请求,那这些是怎么完成的呢?其实这些都是JS在用单线程模拟多线程完成的。

2.png

如上图所示,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()函数。

3.png

宏任务和微任务都有自身的事件循环机制,也拥有独立的事件队列(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线程来完成繁重的计算任务。

4.png

最后给大家出一个考题,可以猜下执行的输出结果来验证学习成果:

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 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中文网其他相关文章!

声明
本文转载于:segmentfault。如有侵权,请联系admin@php.cn删除
Python vs. JavaScript:学习曲线和易用性Python vs. JavaScript:学习曲线和易用性Apr 16, 2025 am 12:12 AM

Python更适合初学者,学习曲线平缓,语法简洁;JavaScript适合前端开发,学习曲线较陡,语法灵活。1.Python语法直观,适用于数据科学和后端开发。2.JavaScript灵活,广泛用于前端和服务器端编程。

Python vs. JavaScript:社区,图书馆和资源Python vs. JavaScript:社区,图书馆和资源Apr 15, 2025 am 12:16 AM

Python和JavaScript在社区、库和资源方面的对比各有优劣。1)Python社区友好,适合初学者,但前端开发资源不如JavaScript丰富。2)Python在数据科学和机器学习库方面强大,JavaScript则在前端开发库和框架上更胜一筹。3)两者的学习资源都丰富,但Python适合从官方文档开始,JavaScript则以MDNWebDocs为佳。选择应基于项目需求和个人兴趣。

从C/C到JavaScript:所有工作方式从C/C到JavaScript:所有工作方式Apr 14, 2025 am 12:05 AM

从C/C 转向JavaScript需要适应动态类型、垃圾回收和异步编程等特点。1)C/C 是静态类型语言,需手动管理内存,而JavaScript是动态类型,垃圾回收自动处理。2)C/C 需编译成机器码,JavaScript则为解释型语言。3)JavaScript引入闭包、原型链和Promise等概念,增强了灵活性和异步编程能力。

JavaScript引擎:比较实施JavaScript引擎:比较实施Apr 13, 2025 am 12:05 AM

不同JavaScript引擎在解析和执行JavaScript代码时,效果会有所不同,因为每个引擎的实现原理和优化策略各有差异。1.词法分析:将源码转换为词法单元。2.语法分析:生成抽象语法树。3.优化和编译:通过JIT编译器生成机器码。4.执行:运行机器码。V8引擎通过即时编译和隐藏类优化,SpiderMonkey使用类型推断系统,导致在相同代码上的性能表现不同。

超越浏览器:现实世界中的JavaScript超越浏览器:现实世界中的JavaScriptApr 12, 2025 am 12:06 AM

JavaScript在现实世界中的应用包括服务器端编程、移动应用开发和物联网控制:1.通过Node.js实现服务器端编程,适用于高并发请求处理。2.通过ReactNative进行移动应用开发,支持跨平台部署。3.通过Johnny-Five库用于物联网设备控制,适用于硬件交互。

使用Next.js(后端集成)构建多租户SaaS应用程序使用Next.js(后端集成)构建多租户SaaS应用程序Apr 11, 2025 am 08:23 AM

我使用您的日常技术工具构建了功能性的多租户SaaS应用程序(一个Edtech应用程序),您可以做同样的事情。 首先,什么是多租户SaaS应用程序? 多租户SaaS应用程序可让您从唱歌中为多个客户提供服务

如何使用Next.js(前端集成)构建多租户SaaS应用程序如何使用Next.js(前端集成)构建多租户SaaS应用程序Apr 11, 2025 am 08:22 AM

本文展示了与许可证确保的后端的前端集成,并使用Next.js构建功能性Edtech SaaS应用程序。 前端获取用户权限以控制UI的可见性并确保API要求遵守角色库

JavaScript:探索网络语言的多功能性JavaScript:探索网络语言的多功能性Apr 11, 2025 am 12:01 AM

JavaScript是现代Web开发的核心语言,因其多样性和灵活性而广泛应用。1)前端开发:通过DOM操作和现代框架(如React、Vue.js、Angular)构建动态网页和单页面应用。2)服务器端开发:Node.js利用非阻塞I/O模型处理高并发和实时应用。3)移动和桌面应用开发:通过ReactNative和Electron实现跨平台开发,提高开发效率。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
4 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
4 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
4 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它们
4 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

螳螂BT

螳螂BT

Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!

mPDF

mPDF

mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),