1. JavaScript 为什么是单线程的?
JavaScript
单线程主要和它的用途有关,它的主要作用是与用户交互以及处理 DOM
,试着想一下如果 JavaScript
是多线程的,一个线程要给一个 DOM
添加元素,另一个线程要删了这个 DOM
,(浏览器一脸懵逼)。所以为为了避免出现这种情况,JavaScript
只能是单线程,也是这门语言的核心。
虽然说后来的 HTML5
提出了 Web Worker
标准,允许 JavaScript
创建多线程,但是这里的子线程完全受主线程控制,并且不能操作 DOM
。这个标准并没有改变 JavaScript
的本质。
2. 同步任务和异步任务
如果都是单线程,那么就会出现一个问题,所有任务都要排队处理,而有的任务执行时间特别的慢(比如 IO
设备),并且 CPU
还有很多空闲的地方,这个时候,还是必须等待上一个任务执行完毕才执行下一个任务。
于是任务就分为两种,同步任务(synchronous
)和异步任务(asynchronous
)。
- 同步任务:在主线程上排队的任务,只有上一个任务执行完毕,才会执行下一个任务。
- 异步任务:不进入主线程,进入任务队列(
task queue
),只有 “任务队列” 通知主线程,某个异步任务可以执行了(并且主线程当前栈已经清空了),该任务才会进入主线程执行。
3. 事件循环机制
上图就是事件循环机制的示例图(JS 运行机制)。
- 首先
JavaScript
主线程运行的时候,产生了堆和栈,这个时候是同步环境。 - 往下执行的时候如果遇到异步任务,就会指给对应的异步进程处理(异步 API)
- 当异步任务(
Ajax
值返回,Timers
时间到达)处理完毕后,会推入任务队列。当异步任务处理完毕后,会推入任务队列。 - 当主线程栈清空的时候(执行完了),会去查看任务队列是否有执行完毕的任务,如果有,就拿出一个任务推入主线程执行(先进先出)
- 重复执行 2、3、4 的这个过程就被称为事件循环。
其中异步任务有:
click
、load
、done
等由浏览器内核的DOM binding
模块处理,事件触发时,回调函数添加到任务队列中。Ajax
网络请求,由浏览器内核模块NetWork
模块处理,网络请求返回后,回调函数添加到任务队列。setTimeout
、setInterval
等,由浏览器内核Timer
模块处理,时间一到,回调函数添加到任务队列。 .
4. 任务队列
如上示意图,任务队列存在多个,同一任务队列内,按队列顺序被主线程取走;不同任务队列之间,存在着优先级,优先级高的优先获取(如用户I/O);
4.1 任务队列的类型
任务队列存在两种类型,一种为 microtask queue
,另一种为 macrotask queue
。
图中所列出的任务队列均为 macrotask queue ,而ES6 的 promise[ECMAScript标准]产生的任务队列为
microtask queue`。
4.2 两者的区别
microtask queue
:唯一,整个事件循环当中,仅存在一个;执行为同步,同一个事件循环中的microtask会按队列顺序,串行执行完毕;
macrotask queue:不唯一,存在一定的优先级(用户I/O部分优先级更高);异步执行,同一事件循环中,只执行一个。
4.3 更完整的事件循环流程
将microtask加入到JS运行机制流程中,则:
- step1、2、3同上,
- step4:主线程查询任务队列,执行
microtask queue
,将其按序执行,全部执行完毕; - step5:主线程查询任务队列,执行
macrotask queue
,取队首任务执行,执行完毕; - step6:重复step4、step5。
microtask queue
中的所有callback处在同一个事件循环中,而macrotask queue中的callback有自己的事件循环。
简而言之:同步环境执行 -> 事件循环1(microtask queue的All
)-> 事件循环2(macrotask queue
中的一个) -> 事件循环->(microtask queue
的All)-> 事件循环2(macrotask queue
中的一个)…
利用 microtask queue
可以形成一个同步执行的环境,但如果 Microtask queue
太长,将导致 Macrotask
任务长时间执行不了,最终导致用户I/O无响应等,所以使用需慎重。
5. 示例、验证
console.log('1, time = ' + new Date().toString())
setTimeout(macroCallback, 0);
new Promise(function(resolve, reject) {
console.log('2, time = ' + new Date().toString())
resolve();
console.log('3, time = ' + new Date().toString())
}).then(microCallback);
function macroCallback() {
console.log('4, time = ' + new Date().toString())
}
function microCallback() {
console.log('5, time = ' + new Date().toString())
}
结合第二节与第三节的分析,此处的执行流程应为:
同步环境:1 -> 2 -> 3
事件循环1(microCallback):5
事件循环2(macroCallback):4
运行结果如下:
运行结果与预期一致,验证了在不同类型的任务队列中,microtask queue中的callball将优先执行。
总结:由此我们了解事件循环的机制,同时了解了任务队列、JS主线程、异步操作之间的相互协作;同时认识了两种任务队列:macrotask queue、microtask queue,它们由不同的标准制定,microtask queue对应ECMAScript的promise属性(ES6)和 DOM3的MutationObserver,文中说明了两者在事件循环中的运行情况及区别;在今后的异步操作中,通过灵活运用不同的任务队列,提升用户交互性能,给出更加的响应和视觉体验;同时,通过JS的事件循环机制,可以更清楚JS代码的执行流,从而更好的控制代码,更有效、更好的为业务服务。
参考:
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
http://www.cnblogs.com/hity-tt/p/6733062.html