先来段代码
const p = new Promise((resolve, reject) => {
console.log(1)
})
console.log(2);
setTimeout(function () {
console.log(3);
}, 1000);
p.then((data) => {
console.log(4)
})
console.log(5);
复制代码
让我们来看看打印结果的顺序
1
2
5
4
3
复制代码
因为javascript是单线程的,只有一个主线程。主线程会先执行同步代码,异步操作会被放入一个任务队列中。等到同步代码执行完成之后,再执行异步任务。 常见的异步操作有:
- Ajax
- DOM的事件操作
- setTimeout
- Promise的then方法
- Node的读取文件
异步任务又分为宏任务
和微任务
。 常见的宏任务有:setTimeout
、setInterval
、setImmediate
、MessageChannel
常见的微任务有:Promise的then方法
、process.nextTick
、MutationObserver
总结javascript执行顺序如下: 1
,若是同步任务,则 主线程中执行;如果是异步任务,就放到一个任务队列里 2
,开始执行主线程中的同步任务,直到将主线程中的所有任务都走完,此时同步任务清空了 3
,回过头看异步队列里如果有异步任务完成了,就生成一个事件并注册回调,放入主线程中 4
,再返回第3步,直到异步队列都清空,程序运行结束
这就是所谓的事件环机制,浏览器和node中的事件环机制有所不同。
浏览器中的事件环
在浏览器的执行环境中,总是先执行微任务,再执行宏任务,再来看看第一段代码,为什么Promise的then方法在setTimeout之前执行?其根本原理就是因为Promise的then方法是一个微任务,而setTimeout是一个宏任务。 执行顺序为: 1
,先执行微任务,清空微任务队列 2
,再执行宏任务,如宏任务中有微任务,则继续执行微任务,直至清空微任务队列 3
,循环上述操作,直至所有任务完成
node中的事件环
图中每一个阶段都代表了一个宏任务队列,在Node事件环中,优先执行微任务
,微任务的运行时机是在每一个“宏任务队列”清空之后,在进入下一个宏任务队列之间执行。这是和浏览器的最大区别。
与浏览器事件环有所不同的是:浏览器事件环是遇到微任务会清空整个微任务队列。 Node事件环是清空完一个阶段的宏任务队列之后再清空微任务队列。
来看一段代码:
const fs = require('fs');
fs.readFile('./1.txt', (err, data) => {
setTimeout(() => {
console.log('timeout');
});
setImmediate(() => {
console.log('immediate');
});
Promise.resolve().then(() => {
console.log('Promise');
});
});
复制代码
执行结果为:
Promise
immediate
timeout
复制代码
代码并不复杂,首先使用fs模块读取了一个文件,在回调的内部有两个宏任务和一个微任务,微任务总是优于宏任务执行的,因此先输出Promise。 但是之后的区别为什么先输出immdiate?原因就在于fs读取文件的宏任务在上图中的第4个轮询阶段,当第4个阶段清空队列之后,就该进入第5个check阶段,也就是setImmediate这个宏任务所在的阶段,而不会跳回第1个阶段,因此先输出immedate。