前言
console.log('代码执行开始...');
setTimeout(() => console.log('定时器开始了...'));
new Promise((resolve, reject) => {
console.log('Promise开始了...');
for (let i = 0; i < 10; i++) {
i === 9 && resolve();
}
}).then(() => console.log('then开始了...'));
console.log('代码执行结束');
// outputs:
// 代码执行开始...
// Promise开始了...
// 代码执行结束
// then开始了...
// 定时器开始了...
JavaScript 是一门单线程的语言,虽然在 H5 中提出了 Web-Worker,但是 JavaScript 是单线程语言这一核心仍然没有改变,但是单线程并不意味着 JavaScript 的执行顺序取决于语句出现的顺序
JavaScript 事件循环
这个时候我们理所当然地会想到同步任务和异步任务,有了同步任务和异步任务,我们就可以导出下面的导图:
- 同步任务和异步任务分别进入不同的执行队列,同步任务进入主线程,异步任务进入 Event Table
- 当指定的操作完成时,Event Table 会将其回调函数注册到 Event Queue
- 主线程的同步任务全部执行完毕之后,去 Event Queue 读取对应的回调函数,将其加入主线程,执行
- 上述3个步骤会不断重复,构成了 Event Loop
JavaScript 引擎中的 monitoring process 进程会执行检查主线程执行栈是否为空,如果为空,就回去 Event Queue 中检查是否存在等待被执行的函数,并将其加入到主线程执行栈,这个操作
setTimeout
在大致了解了 JavaScript 的执行机制之后,对于 setTimeout 函数我们应该有一些新的认值
setTimeout(() => console.log('this is setTimeout function...'), 1000);
sleep(5000); // JavaScript 中并没有封装好的 sleep 函数,这里是举例
很明显,控制台并不会在 1s 后打印 ‘this is setTimeout function…’,而是会在 5s 后打印,回到JavaScript的执行机制,我们会发现其实上述代码是这样执行的:
- setTimeout 进入 Event Table,开始计时
- 主线程执行 sleep 函数,这很慢,比 setTimeout 函数要慢
- 1s 到了,setTimeout 计时完成,其中的箭头函数加入到 Event Queue ,但是 sleep 函数还在执行,等待
- 5s 到了,sleep 函数执行完毕,箭头函数从 Event Queue 中加入到了主线程,开始执行
如果 setTimeout 函数中指定的时间是 0 的话,同理,其也不会立刻执行,而是等待主线程执行栈中没有任务,再加入到主线程执行,需要注意的是,即使主线程执行栈为空,0ms 也是达不到的,最低的时间是 4ms
但问题并没有彻底解决,观察第一个代码块:
console.log('代码执行开始...');
setTimeout(() => console.log('定时器开始了...'));
new Promise((resolve, reject) => {
console.log('Promise开始了...');
for (let i = 0; i < 10; i++) {
i === 9 && resolve();
}
}).then(() => console.log('then开始了...'));
console.log('代码执行结束');
setTImeout 函数中的回调函数和我们新建的 Promise 中的 resolve 函数明显是两个异步任务,而且 setTimeout 函数语句在 resolve 函数语句之前,为何 resolve 函数会比 setTimeout 函数中的回调函数先执行呢?这时,我们需要到导入宏任务和微任务
宏任务和微任务
- 宏任务:整体代码、setTimeout、setInterval、IO、setImmediate、requestAnimationFrame、…
- 微任务:Promise.then catch finally、process.nextTick、MutationObserver、…
不同类型的任务会进入不同的 Event Queue,事件循环的顺序会决定 JavaScript 代码的执行顺序,进入整体代码(宏任务)之后,开始第一次 Event Loop,当前次 Event Loop 中的宏任务执行完毕之后,开始执行微任务,当前次 Event Loop 中的微任务执行完毕之后,从 Event Queue 中拉取一个新的宏任务,开始第二次 Event Loop
回到第一个代码块:
console.log('代码执行开始...');
setTimeout(() => console.log('定时器开始了...'));
new Promise((resolve, reject) => {
console.log('Promise开始了...');
for (let i = 0; i < 10; i++) {
i === 9 && resolve();
}
}).then(() => console.log('then开始了...'));
console.log('代码执行结束');
- 整体代码为宏任务,进入主线程执行栈,console.log(‘代码执行开始…’)
- 遇到 setTimeout 函数,异步宏任务,将其加入宏任务 Event Table,当指定的操作完成时,会将其回到函数加入到 Event Queue
- 遇到 Promise,new Promise 立即执行,console.log(‘Promise开始了…’)
- 遇到 resolve 函数,异步微任务,将其加入微任务 EventTable,当指定操作完成时,将其回调函数也就是 then 函数加入 Event Queue
- 遇到 console.log(‘代码执行结束’),当前 Event Loop 中所有宏任务执行完毕
- 执行当前 Event Loop 中的微任务,console.log(‘then开始了…’)
- 当前 Event Loop 为空,开始下一个 宏任务,console.log(‘定时器开始了…’)
完整的 JavaScript 执行机制如下:
后记
请分析以下代码,作为最后的练习:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
});
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
});
});
process.nextTick(function() {
console.log('6');
});
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8');
});
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
});
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12');
});
});