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

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

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

“单线程”语言

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

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

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

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

立即学习“Java免费学习笔记(深入)”;

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#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; 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 threadstart taskbefore first microtask initfirst microtaskafter first microtask initsecond microtaskfinish first microtaskadd eventend task

登录后复制

本文来自 js教程 栏目,欢迎学习!

以上就是深入理解JavaScript的并发模型和事件循环机制的详细内容,更多请关注【创想鸟】其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至253000106@qq.com举报,一经查实,本站将立刻删除。

发布者:PHP中文网,转转请注明出处:https://www.chuangxiangniao.com/p/2729675.html

(0)
上一篇 2025年3月8日 00:08:54
下一篇 2025年2月27日 04:10:20

AD推荐 黄金广告位招租... 更多推荐

相关推荐

  • 深入学习JavaScript之DOM

    通过html dom,可访问javascript html文档的所有元素。下面本篇文章就来给大家介绍一下,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。 DOM能干啥? ● JavaScript 能够改变页面中的所有 HTM…

    2025年3月8日
    200
  • 深入了解Javascript对象原型

    javascript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。 原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类…

    2025年3月8日
    200
  • 浅谈JS数组Array的用法

    javascript数组用于在单个变量中存储多个值。数组是一个特殊变量,一次可以包含多个值。 【相关课程推荐:JavaScript视频教程】 将数组转换为字符串 JavaScript toString()方法将数组转换为(逗号分隔的)数组值…

    2025年3月8日
    200
  • forEach()、Array.map()和Array.filter()怎么用?(代码示例)

    本篇文章给大家介绍一下foreach()、array.map()和array.filter()的用法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。 Array.forEach() forEach()方法为每个数组元素调用…

    2025年3月8日
    200
  • 面试常问之JavaScript变量提升

    什么是javascript变量提升?这是面试经常会被问到的。下面本篇文章就来给大家介绍一下javascript变量提升,有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。 JavaScript变量提升 提升(Hoisting)…

    2025年3月8日
    200
  • 值得收藏的JavaScript使用小技巧

    任何一门技术在实际中都会有一些属于自己的小技巧。同样的,在使用javascript时也有一些自己的小技巧,只不过很多时候有可能容易被大家忽略。而在互联网上,时不时的有很多同行朋友会总结(或收集)一些这方面的小技巧。 作为一位JavaScri…

    2025年3月8日
    200
  • 详解ES6 Modules

    当下, 我们几乎所有的项目都是基于 webpack、rollup 等构建工具进行开发的,模块化已经是常态。 我们对它并不陌生,今天,我们就再系统的回顾一下ES6的模块机制, 并总结下常用的操作和最佳实践, 希望对你有所帮助。 一些简单的背景…

    2025年3月8日 编程技术
    200
  • 五个超好用的Array.from()用途(详解)

    任何一种编程语言都具有超出基本用法的功能,它得益于成功的设计和试图去解决广泛问题。 JavaScript 中有一个这样的函数:Array.from:允许在 JavaScript 集合(如: 数组、类数组对象、或者是字符串、map 、set …

    2025年3月8日
    200
  • 浅谈前端的正则表达式

    1. 概览 在 JavaScript 中,使用 // 即可创建一个正则表达式对象,当然也可以使用 new RegExp() 常用的跟正则相关的方法有 match、test 和 replace。 其中 match,replace 都是字符串上…

    2025年3月8日 编程技术
    200
  • 浅谈javascript执行机制

    js是单线程的,为什么可以执行异步操作呢? 这归结与浏览器(js的宿主环境)通过某种方式使得js具备了异步的属性。 区分进程和线程: 进程:正在运行中的应用程序。每个进程都自己独立的内存空间。例如:打开的浏览器就是一个进程。 立即学习“Ja…

    2025年3月8日
    200

发表回复

登录后才能评论