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为例,其回调是先进入消息队列(任务队列)进行等待执行,如图:
异步事件被触发或者回调后是进入任务队列,而不是直接执行,需要排队等待前面的事件执行完成后再行执行!
(3)宏任务与微任务:有微则微,无微则宏
JS 中分为两种任务类型:macrotask 和 microtask;
在 ECMAScript 中,macrotask 可称为 task,microtask 称为 jobs。
以上一次事件循环:先运行 macroTask 队列中的一个,然后运行 microTask 队列中的所有任务。接着开始下一次循环(这里只是针对 macroTask 和 microTask,一次完整的事件循环会比这个复杂的多)。
宏任务与微任务的定义?区别?简单点可以按如下理解:
宏任务:可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
每一个 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的关系:
上图大致描述就是:
- 主线程运行时会产生执行栈(调用栈),栈中的代码调用某些Web API 时,当满足触发条件后,它们会在任务队列中添加各种事件(如 ajax 请求完毕)
- 而栈中的代码执行完毕,就会读取任务队列中的事件,去执行那些回调
- 如此循环
注意:总是要等待调用栈中的代码执行完毕后才会去读取(宏)任务队列中的事件!
Ques:js定时器(时钟)的工作机制是怎样的?
setTimeout
和setInterval
之间的差异:
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引擎的工作原理,尤其是在通常发生大量异步事件的情况下,为构建高级应用程序代码奠定了良好的基础。