js单线程

众所周知。JavaScript是以单线程的方式运行的。

一、为什么js是单线程?

这与它的用途有关。

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。若以多线程的方式操作这些DOM,则可能出现操作的冲突。

如果有两个线程同一时候操作一个DOM元素。线程1要求浏览器删除DOM,而线程2却要求改动DOM样式,这时浏览器就无法决定採用哪个线程的操作。当然。我们能够为浏览器引入“锁”的机制来解决这些冲突。但这会大大提高复杂性,所以 JavaScript 从诞生開始就选择了单线程运行。

当然对于不可避免的耗时操作(如:繁重的运算,多重循环)。HTML5提出了Web Worker,它会在当前JavaScript的运行主线程中利用Worker类新开辟一个额外的线程来载入和运行特定的JavaScript文件。这个新的线程和JavaScript的主线程之间并不会互相影响和堵塞运行,并且在Web Worker中提供了这个新线程和JavaScript主线程之间数据交换的接口:postMessage和onMessage事件。但在HTML5 Web Worker中是不能操作DOM的,不论什么须要操作DOM的任务都须要托付给JavaScript主线程来运行。所以尽管引入HTML5 Web Worker。但仍然没有改线JavaScript单线程的本质。

二、并发模式与Event Loop

JavaScript 有个基于“Event Loop”(事件循环)并发的模型。

啊,并发?不是说 JavaScript是单线程吗? 没错。的确是单线程。可是并发与并行是有差别的。前者是逻辑上的同一时候发生,而后者是物理上的同一时候发生。

所以,单核处理器也能实现并发。
这里写图片描写叙述

上图中,主线程运行的时候。产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在”任务队列”中加入各种事件(click,load,done)。

仅仅要栈中的代码运行完成,主线程就会去读取”任务队列”,依次运行那些事件所相应的回调函数。

所谓”回调函数”(callback)。就是那些会被主线程挂起来的代码。异步任务必须指定回调函数。当主线程開始运行异步任务。就是运行相应的回调函数。

运行栈中的代码(同步任务)。总是在读取”任务队列”(异步任务)之前运行。

Stack(栈)

这里放着JavaScript正在运行的任务。每一个任务被称为帧(stack of frames)。

function f(b){
  var a = 12;
  return a+b+35;
}

function g(x){
  var m = 4;
  return f(m*x);
}

g(21);//131

上述代码调用 g 时,创建栈的第一帧,该帧包括了 g 的參数和局部变量。当 g 调用 f 时。第二帧就会被创建,并且置于第一帧之上,当然,该帧也包括了 f 的參数和局部变量。当 f 返回时。其相应的帧就会出栈。同理,当 g 返回时。栈就为空了(栈的特定就是后进先出 Last-in first-out (LIFO))。

Heap(堆)

一个用来表示内存中一大片非结构化区域的名字,对象都被分配在这。

Queue(队列)

一个 JavaScript runtime 包括了一个任务队列。该队列是由一系列待处理的任务组成。而每一个任务都有相相应的函数。

当栈为空时。就会从任务队列中取出一个任务,并处理之。

该处理会调用与该任务相关联的一系列函数(因此会创建一个初始栈帧)。当该任务处理完成后,栈就会再次为空。(Queue的特点是先进先出 First-in First-out (FIFO))。

三、定时器

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制全然一样。差别在于前者指定的代码是一次性运行,后者则为重复运行,即它们的第二个參数是指定其回调函数推迟每隔多少毫秒数后运行。

对于第二个參数有下面须要注意的地方:

  • 当第二个參数缺省时。默觉得0。
  • 在html5中规定,当指定的值小于4毫秒,则添加到4ms。对于2010年及之前的浏览器则是10ms。

下面主要讨论setTimeout()。

console.log(1);
setTimeout(function(){
    console.log(2);
},10);
console.log(3);// 输出:1 3 2

当setTimeout()的第二个參数设为0时,表示当前代码运行完(运行栈清空)以后,马上运行(0毫秒间隔)指定的回调函数。

setTimeout(function(){console.log(1);}, 0);
console.log(2);//2 1

因为仅仅有在运行完第二行以后,系统才会去运行”任务队列”中的回调函数。

总之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空暇时间运行,也就是说。尽可能早得运行。

它在”任务队列”的尾部加入一个事件,因此要等到同步任务和”任务队列”现有的事件都处理完,才会得到运行。

另外,对于那些DOM的变动(尤其是涉及页面又一次渲染的部分),通常不会马上运行,而是每16毫秒运行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。

须要注意的是。setTimeout()仅仅是将事件插入了”任务队列”,必须等到当前代码(运行栈)运行完,主线程才会去运行它指定的回调函数。要是当前代码耗时非常长。有可能要等非常久。所以并没有办法保证,回调函数一定会在setTimeout()指定的时间运行。

四、浏览器是多线程

尽管JS运行在浏览器中。是单线程的,每一个window一个JS线程。但浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步不是单线程的,比如Webkit或是Gecko引擎,都可能有例如以下线程:

  • javascript引擎线程

javascript引擎是基于事件驱动单线程运行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器不管什么时候都仅仅有一个JS线程在运行JS程序。

  • 界面渲染线程

    GUI渲染线程负责渲染浏览器界面,当界面须要重绘(Repaint)或因为某种操作引发回流(reflow)时,该线程就会运行。但须要注意GUI渲染线程与JS引擎是相互排斥的,当JS引擎运行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空暇时马上被运行。

  • 浏览器事件触发线程

    浏览器事件触发线程 事件触发线程,当一个事件被触发时该线程会把事件加入到“任务队列”的队尾。等待JS引擎的处理。这些事件可来自JavaScript引擎当前运行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但因为JS是单线程运行的。全部这些事件都得排队等待JS引擎处理。

  • Http请求线程

    对于Ajax的请求也须要特殊线程来运行。当须要发送一个Ajax请求时。浏览器会开辟一个新的线程来运行HTTP的请求,它并不会堵塞JavaScript线程的运行。当HTTP请求状态变更时,相应事件会被作为回调放入到“任务队列”中等待被运行。

Ajax异步请求是否真的异步?

事实上请求确实是异步的,这请求是由浏览器新开一个线程请求(见前面的浏览器多线程)。当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更事件放到 JavaScript引擎的事件处理队列中等待处理。

当浏览器空暇的时候出队列任务被处理,JavaScript引擎始终是单线程运行回调函数。javascript引擎确实是单线程处理它的任务队列。能理解成就是普通函数和回调函数构成的队列。
总结一下,Ajax请求确实是异步的。这请求是由浏览器新开一个线程请求,事件回调的时候是放入Event loop单线程事件队列等候处理。

看看下面代码:

document.onclick = function(){
    console.log("click")
}

for(var i = 0; i< 100000000; i++);

解释一下代码:首先向document注冊了一个click事件,然后就运行了一段耗时的for循环。在这段for循环结束前,你能够尝试点击页面。当耗时操作结束后,console控制台就会输出之前点击事件的”click”语句。这视乎证明了点击事件(也包括其他各种事件)是由额外单独的线程触发的,事件触发后就会将回调函数放进了“任务队列”的末尾,等待着JavaScript主线程的运行。

五、Node.js的Event Loop

Node.js也是单线程的Event Loop,可是它的运行机制不同于浏览器环境。

这里写图片描写叙述

依据上图。Node.js的运行机制例如以下:

  • V8引擎解析JavaScript脚本。
  • 解析后的代码,调用Node API。
  • libuv库负责Node API的运行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的运行结果返回给V8引擎。
  • V8引擎再将结果返回给用户。

除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与”任务队列”有关的方法:process.nextTick和setImmediate。

process.nextTick方法能够在当前”运行栈”的尾部—-下一次Event Loop(主线程读取”任务队列”)之前—-触发回调函数。

也就是说,它指定的任务总是发生在全部异步任务之前。

setImmediate方法则是在当前”任务队列”的尾部加入事件,也就是说。它指定的任务总是在下一次Event Loop时运行,这与setTimeout(fn, 0)非常像。

引用阮一峰大大的文章。点击查看原文

原文地址:https://www.cnblogs.com/wgwyanfs/p/7227142.html