脑图
下面是详细说明一下事件循环:
首先,明确浏览器是多线程的,而js是单线程,至于为什么是单线程,阮一峰大神的博客是这么说的:
原因大概是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。后来就约定俗成,JavaScript为一种单线程语言。(Worker
API可以实现多线程,但是JavaScript本身始终是单线程的。)
其次,明确Event Loop是一个程序结构,译为事件循环,用于等待和发送消息和事件,主要是连接主线程与负责主线程与其他进程(主要是各种I/O操作)的通信。
再次,明确JavaScript的单线程是主线程+调用栈(即执行栈),主线程负责执行,调用栈将同步任务按照顺序交与主线程执行,异步任务会在调用返回结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。(此步骤为消息队列中执行)
再次,明确调用栈中有两种任务:宏任务、微任务,这个调用栈指的就是消息队列。
- 宏任务:script, settimeout,ajax请求,dom事件;
- 微任务:Promise.then、MutationObserver、process.nextTick();
个人理解,宏任务就是不需要等待返回结果,在此期间可以做别的操作的任务;微任务就是必须等待结果返回才能进行下一步操作的任务。
需记住:同一事件循环中,微任务永远在宏任务之前。
当宏任务执行完,执行栈空闲时,js首先会将微队列中的所有微任务执行完,再去执行宏队列中的宏任务。如此反复,就是事件循环了。
一轮循环是这样的:是一次宏任务加上当前所有微任务执行。当js执行时,先执行宏任务(其实,就是当前宏任务中所有的同步任务),遇到微任务,则推入微队列,并注册回调函数。当该次宏任务执行完后,会读取微任务注册的回调函数到执行栈中执行。执行完后。继续下一轮循环。
例子1:
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
结果:
// one
// two
// three
例子2:
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')
})
})
结果:
// 1
// 7
// 6
// 8
// 2
// 4
// 3
// 5
// 9
// 11
// 10
// 12