一个开放、健全且通用的JavaScript Promise标准。由开发者制定,供开发者参考。

主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。(执行栈、任务队列)
浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程。
1、JavaScript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来然后加以处理,浏览器无论再什么时候都只有一个JS线程在运行JS程序。
2、GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
3、事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在”任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。执行栈中的代码(同步任务),总是在读取”任务队列”(异步任务)之前执行。请看下面这个例子。

1
2
3
4
5
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();

上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取”任务队列”。所以,它与下面的写法等价。

1
2
3
4
5
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};

也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取”任务队列”。
JavaScript对任务的处理机制是:
1、所有同步任务在执行线程上立刻执行。
2、对异步任务,按照事件触发的顺序,依次将回调函数放入异步任务队列里(比如鼠标点击了、定时器设定的时间到了、HTTP请求的状态变了等)。
3、一旦同步任务执行完,执行线程开始读取异步任务队列,依次执行每个任务对应的回调,这个运行机制也称为Event Loop。
重复上面三步,只有在执行线程空闲的情况下,才会去执行异步任务队列中的任务(其中事件会优先执行)。
Node.js也是单线程的Event Loop。

根据上图,Node.js的运行机制如下:
(1)V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。
setTimeout(fn, 0)的主要作用是,改变了代码流程,把fn的执行放在了当前同步代码全部执行完之后。另外,在HTML5规范中setTimeout的延时的最小值为4毫秒。
除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与”任务队列”有关的方法:process.nextTick和setImmediate。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	setTimeout(function(){
for(var i = 0; i < 100000000; i++){}
console.log('timer a');
}, 0)

for(var j = 0; j < 5; j++){
console.log(j);
}

setTimeout(function(){
console.log('timer b');
}, 0)

function waitFiveSeconds(){
var now = (new Date()).getTime();
while(((new Date()).getTime() - now) < 5000){}
console.log('finished waiting');
}

document.addEventListener('click', function(){
console.log('click');
})

console.log('click begin');
waitFiveSeconds();
//答案
0
1
2
3
4
click begin
finished waiting
click
click
timer a
timer b
click
click

  • 这里提及了macrotask和microtask两个概念,这表示异步任务的两种分类。在挂起任务时,JS引擎会将所有任务按照类别分到这两个队列中,首先在macrotask的队列(这个队列也被叫做task queue)中取出第一个任务,执行完毕后取出microtask队列中的所有任务顺序执行;之后再取macrotask任务,周而复始,直至两个队列的任务都取完。
    两个类别的具体分类如下:1、macro-task: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering 2、micro-task: process.nextTick, Promises(这里指浏览器实现的原生 Promise), Object.observe, MutationObserver
    注意:先从macrotask取,再从microtask取;macrotask queue是一次取一个,microtask queue是一次取所有(下次如无,则不取);
  • 为了协调异步任务,Node居然提供了四个定时器,让任务可以在指定的时间运行。1、setTimeout() 2、setInterval() 3、setImmediate() 4、process.nextTick() 前两个是语言的标准,后两个是Node独有的。
  • Node规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。
  • 根据语言规格,Promise对象的回调函数,会进入异步任务里面的”微任务”(microtask)队列。微任务队列追加在process.nextTick队列的后面,也属于本轮循环。注意,只有前一个队列全部清空以后,才会执行下一个队列。本轮循环的执行顺序:1、同步任务 2、process.nextTick() 3、微任务
  • 次轮循环的执行顺序: 事件循环会无限次地执行,一轮又一轮。只有异步任务的回调函数队列清空了,才会停止执行。每一轮的事件循环,分成六个阶段。这些阶段会依次执行。每个阶段都有一个先进先出的回调函数队列。只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。

    1、timers:这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。
    2、I/O callbacks:除了以下操作的回调函数,其他的回调函数都在这个阶段执行。setTimeout()和setInterval()的回调函数、setImmediate()的回调函数、用于关闭请求的回调函数,比如socket.on(‘close’, …)
    3、idle, prepare:该阶段只供libuv内部调用,这里可以忽略。
    4、Poll:这个阶段是轮询时间,用于等待还未返回的I/O事件。这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器)会一直停留在这个阶段,等待I/O请求返回结果。
    5、check:该阶段执行setImmediate()的回调函数。
    6、close callbacks:该阶段执行关闭请求的回调函数,比如socket.on(‘close’, …)。
    //注:a、官方文档的一个示例经历了三轮事件循环。b、setTimeout在timers阶段执行,而setImmediate在check阶段执行。

Comments

去留言
2017-02-21

⬆︎TOP