Ques:什么是js单线程?

  • 进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是 cpu 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

【提示】

  • 不同进程之间也可以通信,不过代价较大
  • 单线程与多线程,一般都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)

JavaScript 语言的一大特点就是单线程,其在同一个时间内只能做一件事。

JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的线程同步问题。比如,假定JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

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

 

Ques:为什么js又存在异步操作?

 

浏览器的内核是多线程的,一个浏览器一般至少实现三个常驻线程

  • javascript引擎:是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。
  • GUI渲染线程:负责渲染浏览器界面,当界面需要重排、重绘或由于某种操作引发回流时,该线程就会执行。但需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  • 事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理

 

Ques:js异步机制及其原理?

 

(1)js的工作机制:

当线程中没有执行任何同步代码的前提下才会执行异步代码,setTimeout是异步代码,所以setTimeout只能等js空闲才会执行,但如果在其之前存在死循环,死循环是永远不会空闲的,所以setTimeout也永远不会执行。即使setTimeout为0,他也是等js引擎的代码执行完之后才会插入到js引擎线程的最后执行。

 

(2)js是如何处理异步操作的?

js 引擎执行异步代码而不用等待,是因有为有 消息队列和事件循环。

  • 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
  • 事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。

实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。

当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做js事件循环机制,取一个消息并执行的过程叫做一次循环。

以Ajax为例,其回调是先进入消息队列(任务队列)进行等待执行,如图:

java单线程读单线程写文件 单线程js_java单线程读单线程写文件

异步事件被触发或者回调后是进入任务队列,而不是直接执行,需要排队等待前面的事件执行完成后再行执行!

 

(3)宏任务与微任务:有微则微,无微则宏

JS 中分为两种任务类型:macrotask 和 microtask;

在 ECMAScript 中,macrotask 可称为 task,microtask 称为 jobs。

 

java单线程读单线程写文件 单线程js_java单线程读单线程写文件_02

 

以上一次事件循环:先运行 macroTask 队列中的一个,然后运行 microTask 队列中的所有任务。接着开始下一次循环(这里只是针对 macroTask 和 microTask,一次完整的事件循环会比这个复杂的多)。

java单线程读单线程写文件 单线程js_消息队列_03

宏任务与微任务的定义?区别?简单点可以按如下理解:

 

宏任务:可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调放到执行栈中执行

每一个 task 会从头到尾将这个任务执行完毕,不会执行其它代码;

浏览器为了能够使得 JS 内部 task 与 DOM 任务能够有序的执行,会在一个 task 执行结束后,在下一个 task 执行开始前,对页面进行重新渲染(task -> 渲染 -> task ->...)

 

微任务:可以理解是在当前 task 执行结束后立即执行的任务

也就是说,在当前 task 任务后下一个 task 之前在渲染之前

所以它的响应速度相比 setTimeout(setTimeout是task)会更快,因为无需等渲染

也就是说,在某一个 macrotask 执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)。

 

分别怎样的场景会形成 macrotask 和 microtask 呢 ?

宏任务: 

主代码块;

setTimeout;
setInterval;
setImmediate;
requestAnimationFrame;
I/O;
UI rendering;

 

可见:事件队列中的每一个事件都是一个宏任务

 

微任务: 

process.nextTick;
Promise;
Object.observe;
MutationObserver;

 

【补充】

[1] 在 node 环境下process.nextTick 的优先级高于 Promise,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的 nextTickQueue 部分,然后才会执行微任务中的 Promise 部分。

[2] setImmediate 则是规定:在下一次 Event Loop(宏任务)时触发(所以它是属于优先级较高的宏任务),(Node.js 文档中称,setImmediate 指定的回调函数,总是排在 setTimeout 前面),所以 setImmediate 如果嵌套的话,是需要经过多个 Loop 才能完成的,而不会像 process.nextTick 一样没完没了。

 

关于调用栈、(宏)任务队列、微任务队列、Web APIs的关系:

java单线程读单线程写文件 单线程js_单线程_04

java单线程读单线程写文件 单线程js_java单线程读单线程写文件_05

上图大致描述就是:

  • 主线程运行时会产生执行栈(调用栈),栈中的代码调用某些Web API 时,当满足触发条件后,它们会在任务队列中添加各种事件(如 ajax 请求完毕)
  • 而栈中的代码执行完毕,就会读取任务队列中的事件,去执行那些回调
  • 如此循环

注意:总是要等待调用栈中的代码执行完毕后才会去读取(宏)任务队列中的事件!

 

Ques:js定时器(时钟)的工作机制是怎样的?

java单线程读单线程写文件 单线程js_java单线程读单线程写文件_06

setTimeoutsetInterval之间的差异:


setTimeout(function(){
  /* Some long block of code... */
  setTimeout(arguments.callee, 10);   //callee:调用参数为arguments的函数,多用于解耦
}, 10);
 
setInterval(function(){
  /* Some long block of code... */
}, 10);

乍看之下,这两段代码在功能上似乎是等效的,但事实并非如此。值得注意的是,setTimeout代码在上一次执行回调之后将始终至少有10ms的延迟(最终可能会更多,但永远不会更少),而setInterval无论最后一次执行回调的时间如何,都会尝试每10ms执行一次回调(在队列中存在未执行的该interval调用时会舍弃其之后的调用)。


我们在这里学到了很多东西,让我们回顾一下:

  • JavaScript引擎只有一个线程,从而迫使异步事件排队等待执行。
  • setTimeout并且setInterval它们执行异步代码的方式从根本上不同。
  • 如果某个setTimeout的回调被阻止立即执行,它将被延迟到下一个可能的执行点(比所需的延迟时间更长)。
  • 如果Interval执行的时间足够长(比指定的延迟时间长),则Interval可以不延迟地连续执行。

所有这些都是非常重要的基础知识。了解JavaScript引擎的工作原理,尤其是在通常发生大量异步事件的情况下,为构建高级应用程序代码奠定了良好的基础。