热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JavaScript的单线程和事件循环是什么

JavaScript的单线程和事件循环是什么?针对这个问题,今天小编总结了这篇文章,希望能帮助更多想解决这个问题的朋友找到更加简单易行的办法。单线

Javascript的单线程和事件循环是什么?针对这个问题,今天小编总结了这篇文章,希望能帮助更多想解决这个问题的朋友找到更加简单易行的办法。

单线程就是进程只有一个线程,线程较多线程来说,系统稳定、扩展性极强、软件丰富。多用于点对点的服务。

"单线程"语言

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

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

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

JS数据结构

Javascript的单线程和事件循环是什么

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

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

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

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

事件循环

Javascript是单线程的语言,也就是说,同一个时间只能做一件事。而这个单线程的特性,与它的用途有关,作为浏览器脚本语言,Javascript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定Javascript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许Javascript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变Javascript单线程的本质

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

Javascript的单线程和事件循环是什么

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

Javascript的单线程和事件循环是什么

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

Javascript的单线程和事件循环是什么

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

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

关于Javascript的单线程和事件循环是什么就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。


推荐阅读
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • 初识java关于JDK、JRE、JVM 了解一下 ... [详细]
  • 尾部|柜台_Java并发线程池篇附场景分析
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java并发-线程池篇-附场景分析相关的知识,希望对你有一定的参考价值。作者:汤圆个人博客 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 加密世界下一个主流叙事领域:L2、跨链桥、GameFi等
    本文介绍了加密世界下一个主流叙事的七个潜力领域,包括L2、跨链桥、GameFi等。L2作为以太坊的二层解决方案,在过去一年取得了巨大成功,跨链桥和互操作性是多链Web3中最重要的因素。去中心化的数据存储领域也具有巨大潜力,未来云存储市场有望达到1500亿美元。DAO和社交代币将成为购买和控制现实世界资产的重要方式,而GameFi作为数字资产在高收入游戏中的应用有望推动数字资产走向主流。衍生品市场也在不断发展壮大。 ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
author-avatar
倒叙的时光_SX_126
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有