JavaScript单线程的运行机制

版权声明:如有转载,请注明出处~ https://blog.csdn.net/weixin_38080573/article/details/79376170

每一种语言都有自己的运行机制,javascript当然也不例外,了解了js的运行机制,对于想要深入学习js的我们来说有着莫大的帮助。好了话不多说,下面开始讲述一js的整个运行机制,但是在此之前你先要了解几个概念。下面一一列举,并配合实例来帮助大家理解记忆。

一、js是一门单线程的语言

 那什么是单线程呢。?

  单线程通俗的来说就是同一时间只能做一件事情,不能同时做几件事情,与传统的后台编程语言如java一般都是多线程的可以同时做几件事情,也就是所谓的并发。  说道为什么不把js设计为多线程并发的呢。?这样不是功能更加强大么。?这是因为在js的设计之初就是作为浏览器的脚本语言,主要作用就是与用户做交互,如果设计为多线程的话,那就会带来很多复杂的同步问题了,势必会影响浏览器的交互效果,不妨假设一下若js有两个线程,A线程需要将页面信息删除,B线程需要将页面信息修改显示,那就会让前端处理逻辑变得复杂了,也就会影响与用户的交互体验了。

所以从javascript诞生之初就被设计为单线程语言,估计这个特性在以后也不会被改变。

当然现在HTML5提出了Web Worker标准,允许javascript创建多线程,但是创建的子线程完全受主线程控制。而且子线程是不能访问DOM的,子线程的全局对象也不是window。这样当前端有一些巨大的数据计算的时候,可以充分利用多核CPU的计算能力。关于web worker后期我们会专门有一篇文章来讲述,在此不做过多说明。

    说到后端语言的并发插入一些额外的说明,与本文索要阐述的js单线程无关,以前还只是单核处理器的时候,其实大多是“伪并发”它是通过操作系统快速地在各个计算任务之间切换从而向用户展示一种“这些计算任务正在被并发执行”的假象,实际上,在某一确定的时间点上,只有一个线程被运行着。这是运行的速度足够的块,给用户感觉这些任务好像是同时进行的,到后来多核处理器普及了,由于系统中同时存在着多个处理器,那么我们就可以让多个线程在同一时刻同时运行在不同的处理器上。


二、什么是任务队列

由于JS是一个单线程的语言,就需要一个专门来管理任务执行的队列,只有前一个任务执行完了,才会接着去执行下一个任务。但是有些任务耗费的时间很长,如果是cpu一直在忙碌在干事情,那倒也没什么,最怕的是此时cpu是处于空闲状态的。比如I/O操作,或者Ajax请求,有时候我们发出一个ajax到后端请求数据到浏览器接受到后端返回的数据,这段时间比较长。此时若是cpu一直处于空闲等待的状态的话,那岂不是会浪费很多性能。所以设计者就考虑不妨将这些需要等待的任务挂起,去执行后面的任务,等到所请求的数据返回或者I/O操作结束有了结果之后再回过头去执行刚刚被挂起的任务。

因此根据上述我们又可以将任务分为两类,一种是同步任务,一种是异步任务。我们将同步任务放在浏览器的主线程之上,只有前面一个任务执行完成之后才会执行后面的任务,异步任务是指 不进入主线程,而进入任务队列的任务。只有任务队列通知主线程,某个异步任务可以执行了,那么此时该任务才会进入主线程执行。

具体来说异步任务的执行机制如下:

1:有一个主线程用于执行一些同步任务,形成一个执行栈。

2:与此同时还有一个任务队列,当异步任务有了运行结果的时候,就会在任务队列中放置一个事件。

3:一旦主线程上的同步任务都执行完了,系统就会依次读取任务队列上的任务。而此时正处于“等待”状态的异步任务也就结束等待了,开始进入执行栈中,开始执行。

4:主线程不断反复的重复上面第三部。

一旦主线程上面的任务运行完之后就会从任务队列中读取新的任务继续运行。这就是javascript的运行机制,整个过程不断循环反复直至所有任务全部运行结束。

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。


三、回调函数

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。若是一个异步任务没有回调函数的话,那等异步任务返回数据后,执行栈执行什么呀。(⊙﹏⊙)

一般而言,任务队列是一个队列的数据结构(即先进先出),排在前面位置的任务会被主线程优先的读取执行。主线程从任务队列读取的过程基本上是自动的,即当主线程上任务执行完之后,就会自动从任务队列上读取任务去执行。


四、Event Loop

主线程从任务队列循环不断读取任务的这个过程我们称之为Event Loop循环即事件循环。

下面配一张图(图片来源于阮一峰老师的博客)来帮助大家更好的理解整个JS的运行机制




我们可以看到,这个V8引擎在解析的时候有一个任务栈,任务栈可以调用各种外部API来,而他们又可以不断的像任务队列中增加各种事件(click,load...)只要主线程运行完之后就会读取任务队列上的回调函数。

var a = 10;
var arr = []
$.get(url,{}, callback);
arr.push(a)
上面这demo中的回调函数与向arr中增加数字a的执行顺序和下面代码的执行是完全一样的

var a = 10;
var arr = [];
arr.push(a);
$.get(url,{},callback)

因为浏览器总是会先执行主线程上的任务,之后再会执行异步任务的回调函数。即总是先执行arr.push(a)然后在执行回调函数callback,所以一般都会将这个异步请求先发送出去。然后再去执行同步任务。


五、说说定时器

我们一般常用的定时器是setTimeout()和setInterval(),他么的内部机制以及传参是一模一样,区别在于前者只会执行一次,而后者会在一定的时间段之后反复执行。很多初学者都认为用定时器在设置了一定时间之后就会执行定时器中的回调函数,那了解了js的运行机制之后,严格来说并不是这样。我们以setTimeout()为例说明

请参考如下代码

console.log('A')
    
   setTimeout(function () {
       console.log('B')
   },1000)

    console.log('C')

然后我们看看输出效果

    

我们会发现打印出的顺序总是ACB而不是ABC,这是因为定时器的作用是在1s中之后将回调函数放在任务队列之中,注意,这儿并没有说1s中之后就会执行回调函数,而是说放到任务队列之后,那如果此时任务队列中还有其他的异步任务的回调的话,那就不会执行定时器的回调,而是按照我们所说的主线程会按照次序依次读取任务队列中的回调,没错,这其实也就意味着,虽然你设置的是1s之后执行一个打印字符串'B'的操作,可实际上在打印的时候未必会是1s之后有可能是1s+,这个+是多少回取决于当前任务队列中是否还有以及有多少其他的回调函数。这与我们平时对定时器的认知恐怕有点不一样。不过读完这篇文章了解了js的运行机制,一切也就了然于胸了。另外说明一下,HTML5规定了定时器的第二个参数不得小于4毫秒,如果小于这个值得话会自动增加。以前老版本是10毫秒,然而对于DOM的变动(尤其是设计页面重新渲染的部分)是16毫秒执行一次。这个时候使用requestAnimationFrame()的效果要好于setTimeout() 的效果。












猜你喜欢

转载自blog.csdn.net/weixin_38080573/article/details/79376170