一篇文章让你搞清楚JavaScript事件循环
作者:诸葛小愚 时间:2024-04-19 09:53:02
前言
异步函数也是有执行顺序的。本质上来说,JavaScript是单线程语言,不管是在浏览器中还是nodejs环境下。浏览器在执行js代码和渲染DOM节点都是在同一个线程中,执行js代码就无法渲染DOM,渲染DOM的时候就无法执行js代码。如果按照这种同步方式执行,页面的渲染将会出现白屏甚至是报错,特别是遇到一些耗时比较长的网络请求或者js代码,因此在实际开发中一般是通过异步的方式解决。
什么是异步?js是一步一步执行代码的,遇到alert这种阻塞代码时,js将会停止往下执行直到阻塞代码执行完毕。异步就是将函数放在单独的异步队列中,不会产生阻塞,js可以继续往下执行,等到同步代码执行完毕后再执行异步队列中的函数。因此,js会先执行完同步代码,才会执行异步代码。异步函数之间,虽然都是异步,但是还是有相对的执行顺序。
异步函数的执行主要依靠事件循环来处理,本文重点探讨异步的分类(宏任务、微任务)、事件循环以及异步函数的执行顺序。
宏任务
宏任务,也可简单的说成是任务,在下一轮DOM渲染之后执行。常见的宏任务有:
setTimeout:设置一个定时器,该定时器会在设置的延迟时间到期后执行一个函数或者指定的代码块。值得注意的是,setTimeout不一定会在延迟时间到达后就立即执行函数,而是会判断执行队列中是否还有函数没有处理,如果没有了并且栈为空,setTimeout才会在延迟时间到达后执行函数。
// setTimeout 延迟执行不等于到期时立即执行
let now = new Date().getSeconds();
setTimeout(() => {
console.log('this is setTimeout 0');
}, 0);
setTimeout(() => {
console.log('this is setTimeout 200');
}, 200);
while(true) {
if (new Date().getSeconds() - now >= 2) {
console.log('break out while loop');
break;
}
}运行结果
break out while loop
this is setTimeout 0
this is setTimeout 200先执行同步代码,再执行异步。setTimeout(() => {}, 0)表示0毫秒后立即执行函数,但是当前执行队列中还有未处理完的while循环,因此需要等到while循环执行完毕后,才会根据延迟到期时间执行函数。
setInterval:设置定时器,表示在固定的时间间隔内,重复执行某一函数或者特定的代码块。注意使用setInterval有最小延迟时间限制以及确保执行时间要小于间隔时间,如果执行时间无法确定,则应采用递归调用setTimeout的方式代替。
网络请求:只要是指XMLHttpRequest等网络请求
微任务
微任务,在下一轮DOM渲染之前执行,微任务比宏任务更早执。常见的微任务有:
promise:表示一个异步操作最终的结果和返回值,可能会失败,也可能成功。异步函数在执行时,什么时候返回结果是不可预料的,Promise把异步操作的返回值和函数关联起来,保证在异步执行结束后会执行对应的函数,并通过函数返回操作值。这种效果就类似于把异步代码“同步执行”。
queueMicrotask:将函数添加到微任务队
console.log('start');
// 微任务队列
Promise.resolve().then(() => {
console.log('promise then');
});
queueMicrotask(() => {
console.log('queueMicrotask');
});
console.log('end');
运行结果
start
end
promise then
queueMicrotask
事件循环
因为有异步操作的存在,所以出现了事件循环,如果都是同步操作,一行一行执行代码,事件循环也就失去了用武之地。在了解事件循环前,还需要补充js的执行过程:
js在执行代码时,遇到函数就会将其添加到调用栈中,每一帧都会存储当前函数的参数和局部变量,当一个函数执行完毕,则会从调用栈中弹出,直到栈被清空,那么程序也就执行完毕。在执行的过程中,需要的引用数据都是从堆中获取。
在实际开发中,往往是同步代码和异步代码都有。在js执行时,还是从第一行代码开始执行,遇到函数就将其添加到栈中,然后执行同步操作;如果遇到异步函数,则根据其类型,宏任务就添加到宏任务队列,微任务添加到微任务队列。直到同步代码执行完毕,则开始执行异步操作。
异步操作后于同步操作,异步操作内部也是分先后顺序的。总的来说:
微任务先于宏任务执行
微任务与微任务之间根据先后顺序执行,宏任务与宏任务之间根据延迟时间顺序执行
微任务在下一轮DOM渲染前执行,宏任务在下一轮DOM渲染之后执行
每个任务的执行都是一次出栈操作,直到栈被清空
微任务比宏任务先执行
console.log('start');
// 宏任务队列
setTimeout(() => {
console.log('setTimeout');
});
// 微任务队列
Promise.resolve().then(() => {
console.log('promise then');
});
console.log('end');
// 执行结果
start
end
promise then
setTimeout
微任务在下一轮DOM渲染前执行,宏任务在之后执行
let div = document.createElement('div');
div.innerHTML = 'hello world';
document.body.appendChild(div);
let divList = document.getElementByTagName('div');
console.log('同步任务 length ---', list.length);
console.log('start');
setTimeout(() => {
console.log('setTimeout length ---', list.length);
alert('宏任务 setTimeout 阻塞'); // 使用alert阻塞js执行
});
Promise.resolve().then(() => {
console.log('promise then length ---', list.length);
alert('微任务 promise then 阻塞);
});
console.log('end');
事件循环
event loop会持续监听是否有异步操作,如果有则添加到对应的队列中,等待执行。例如在宏任务中添加微任务,或者在微任务中添加宏任务,当前任务执行完后,可能还会有新的任务添加到事件循环中。
宏任务与微任务
微任务中创建宏任务
new Promise((resolve) => {
console.log('promise 1');
setTimeout(() => {
console.log('setTimeout 1');
}, 500);
resolve();
}).then(() => {
console.log('promise then');
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
});
new Promise((resolve) => {
console.log('promise 2');
resolve();
})运行结果
promise 1
promise 2
promise then
setTimeout 2
setTimeout 1解析
js执行代码,遇到两个Promise,则分别添加到微任务队列,同步代码执行完毕。
在微任务队列中根据先进先出,第一个Promise先执行,遇到setTimeout,则添加到宏任务队列,
resolve()
返回执行结果并执行then,事件循环将其继续添加到微任务队列;第一个Promise执行完毕,执行第二个Promise。继续执行微任务队列,直到清空队列。遇到setTimeout,并将其添加到宏任务队列
宏任务队列现在有两个任务待执行,由于第二个setTimeout的延迟事件更小,则优先执行第二个;如果相等,则按照顺序执行。
继续执行宏任务队列,直到清空队列。
宏任务中创建微任务
setTimeout(() => {
console.log('setTimeout 1');
new Promise((resolve) => {
console.log('promise 1');
resolve();
}).then(() => {
console.log('promise then');
})
}, 500);
setTimeout(() => {
console.log('setTimeout 2');
new Promise((resolve) => {
console.log('promise 2');
resolve();
})
}, 0);运行结果
setTimeout 2
promise 2
setTimeout 1
promise 1
promise then解析
js执行代码,遇到两个setTimeout,将其添加到宏任务队列,同步代码执行完毕
先检查微任务队列中是否有待处理的,刚开始肯定没有,因此直接执行宏任务队列中的任务。第二个为零延迟,需要优先执行。遇到Promise,将其添加到微任务队列。第一个宏任务执行完毕
在执行第二个宏任务时,微任务队列中已经存在待处理的,因此需要先执行微任务。
微任务执行完毕,并且延迟时间到期,第一个setTimeout开始执行。遇到Promise,将其添加到微任务队列中
执行微任务队列中的Promise,执行完毕后遇到then,则将其继续添加到微任务队列
直到所有微任务执行完毕
宏任务中创建宏任务
setTimeout(() => {
console.log('setTimeout 1');
setTimeout(() => {
console.log('setTimeout 2');
}, 500);
setTimeout(() => {
console.log('setTimeout 3');
}, 500);
setTimeout(() => {
console.log('setTimeout 4');
}, 100);
}, 0);运行结果
setTimeout 1
setTimeout 4
setTimeout 2
setTimeout 3解析
宏任务中创建宏任务,执行顺序一般来说是按照先后顺序的。对于setTImeout来说,延迟时间相同,则按照先后顺序执行;延迟时间不同,则按照延迟时间的大小先后顺序执行
微任务中创建微任务
new Promise((resolve) => {
console.log('promise 1');
new Promise((resolve) => {
console.log('promise 2');
resolve();
});
new Promise((resolve) => {
console.log('promise 3');
resolve();
})
resolve();
})运行结果
promise 1
promise 2
promise 3解析
微任务中创建微任务,执行顺序一般来说是按照先后顺序执行的。
总结
同步代码直接执行,异步代码添加到宏任务队列或者微任务队列
微任务在下一轮DOM渲染前执行,宏任务在下一轮DOM渲染之后执行
事件循环持续监听
如果存在异步操作,需要将关联代码放在异步函数中执行;或者将异步函数转为同步操作
如果代码层次比较复杂,同步、异步代码混杂,一定要理清代码的执行顺序。避免因为异步,导致代码出现难以察觉的bug
参考资料
ajax请求是宏任务还是微任务一篇搞定(Js异步、事件循环与消息队列、微任务与宏任务)
深入:微任务与Javascript运行时环境 - Web API 接口参考 | MDN (mozilla.org)
在 JavaScript 中通过 queueMicrotask() 使用微任务 - Web API 接口参考 | MDN (mozilla.org)
并发模型与事件循环 - JavaScript | MDN (mozilla.org)
来源:https://juejin.cn/post/7094630620076195871/