JS 只有一个主线程,主线程执行完执行栈的任务后,去检查异步的任务队列,如果异步事件触发,则将其加到主线程的执行栈。这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环)。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
除了放置异步任务的事件,"任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。
定时器功能主要由 setTimeout()
和 setInterval()
这两个函数来完成。
HTML5 标准规定了 setTimeout() 的第二个参数的最小值(最短间隔),不得低于 4 毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为 10 毫秒。
另外,对于那些 DOM 的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每 16 毫秒执行一次。这时使用 requestAnimationFrame()
的效果要好于 setTimeout()
。
Node.js 提供了另外两个与"任务队列"有关的方法:process.nextTick
和 setImmediate
。
process.nextTick 方法可以在当前"执行栈"的尾部----下一次 Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。
process.nextTick(function A() {
console.log(1);
process.nextTick(function B() {
console.log(2);
});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
// 1
// 2
// TIMEOUT FIRED
setImmediate 方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次 Event Loop 时执行,这与 setTimeout(fn, 0)很像。
setImmediate(function A() {
console.log(1);
setImmediate(function B() {
console.log(2);
});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
// 1--TIMEOUT FIRED--2 or
// TIMEOUT FIRED--1--2
上述代码封装在 setImmediate() 时,结果固定了。
Node.js 文档中称,setImmediate 指定的回调函数,总是排在 setTimeout 前面。实际上,这种情况只发生在递归调用的时候。
setImmediate(function() {
setImmediate(function A() {
console.log(1);
setImmediate(function B() {
console.log(2);
});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
// 1--TIMEOUT FIRED--2
多个 process.nextTick 语句总是在当前"执行栈"一次执行完,多个 setImmediate 可能则需要多次 loop 才能执行完。
另外,由于 process.nextTick 指定的回调函数是在本次"事件循环"触发,而 setImmediate 指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。
在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。
在行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行完,而 setImmediate()在每轮循环中执行链表中的一个回调函数。
process.nextTick(function() {
console.log('nextTick延迟执行1');
});
process.nextTick(function() {
console.log('nextTick延迟执行2');
});
setImmediate(function() {
console.log('setImmediate延迟执行1');
process.nextTick(function() {
console.log('强势插入');
});
});
setImmediate(function() {
console.log('setImmediate延迟执行2');
});
console.log('正常执行');
// console.log("正常执行");
// nextTick延迟执行1
// nextTick延迟执行2
// setImmediate延迟执行1
// setImmediate延迟执行2
// 强势插入
任务队列又分为 macro-task(宏任务)与 micro-task(微任务),在最新标准中,它们被分别称为 task 与 jobs。
1. macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
1. micro-task大概包括: process.nextTick, Promise, MutationObserver(html5新特性)
setTimeout/Promise 等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
setTimeout 是一个宏任务源,写在里面的回调函数会加到宏任务队列中。
Promise 是一个微任务源,写在里面 resolve 以及 reject 回调会被加到微任务队列中。
事件循环可以分为这样的一个过程:分别是 宏任务->执行栈->微任务。
setTimeout(function() {
console.log('timeout1');
});
new Promise(function(resolve) {
console.log('promise1');
for (var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function() {
console.log('then1');
});
console.log('global1');
// promise1
// promise2
// global1
// then1
// timeout1
console.log('golb1');
setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
});
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then');
});
});
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
});
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then');
});
});
process.nextTick(function() {
console.log('glob1_nextTick');
});
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then');
});
setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
});
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then');
});
});
process.nextTick(function() {
console.log('glob2_nextTick');
});
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then');
});
setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
});
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then');
});
});
// nextTick队列 会比 Promise.then队列先执行
// setImmediate 的任务队列会在 setTimeout 队列的后面执行
// log:
// golb1 glob1_promise glob2_promise
// glob1_nextTick glob2_nextTick glob1_then glob2_then
// timeout1 timeout1_promise timeout2 timeout2_promise
// timeout1_nextTick timeout2_nextTick timeout1_then timeout2_then
// immediate1 immediate1_promise immediate2 immediate2_promise
// immediate1_nextTick immediate2_nextTick immediate1_then immediate2_then