js 事件循环也称为js运行机制,是js单线程(一段时间内只能做一件事)无阻塞运行的核心技术。js单线程编程主要是由其用途决定的,作为浏览器语言,主要用途是与用户交互、操作dom。所以如果js是多线程,同一时间一个线程在dom节点添加子节点,另个线程删除该dom节点,两者冲突js引擎就不知道要如何操作了。
1、了解js事件循环机制,首先要清楚调用堆栈(调用栈)、同步任务、异步任务、宏任务、微任务等各代表什么意思。
1)调用栈
调用栈也称执行栈,具有后进先出的结构,用于存储在代码执行期间创建的所有执行上下文(可以理解成代码执行所在的环境,主要由代码执行前的准备工作组成,例如变量提升等)。当JS引擎开始执行第一行脚本代码时,就会创建一个全局执行上下文(只在页面或浏览器关闭时才出栈)然后压入执行栈中,每当引擎遇到一个函数时,就会创建一个函数执行上下文,然后将这个执行上下文压入到执行栈。引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程就会到达执行栈的下一个执行上下文。
函数执行过程实际上就是创建执行上下文、入栈、执行、出栈的过程。
示例:
console.log(0);
function first() {
console.log(1);
second();
}
function second() {
console.log(2);
}
console.log(3);
first();
//输出内容 0 3 1 2
实例代码分析过程如下:
①创建全局上下文并入栈(可以理解成Java中main函数入口)。
②执行全局上下文过程中遇到first函数,创建first函数执行上下文并入栈。
③执行first过程中遇到second,创建second的函数执行上下文并入栈。
④second执行完毕后出栈。
⑤first执行完毕后出栈,此时调用栈只剩下全局上下文,且不出栈,如有异步任务则将异步任务入栈并执行,直到页面后浏览器关闭时全局上下文出栈。
2)同步任务、异步任务
因为js是单线程,如果遇到一个任务执行之间较长(文件读取操作或者ajax请求),那么后面的任务都必须等待,用户体验会很差。所以js设计时,主线程会将ajax请求等先挂起(进入任务队列),执行后面的任务,当ajax数据返回后再继续执行被挂起的任务,这就产生了异步任务和同步任务。
同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。
异步任务由JavaScript 委托给宿主环境进行执行,当异步任务执行完成后,会通知JavaScript 主线程执行异步任务的回调函数。异步任务是不进入主线程,而进入任务队列的任务(都是回调函数),只有任务队列通知主线程,某个异步任务可以执行了(此时达到异步任务执行条件,例如ajax请求返回了数据),该任务才会进入主线程(此时调用栈只有一个全局上下文,没有待执行的同步任务,如果有待执行的同步任务,主线程必须先执行同步任务,异步任务需等待入栈并被执行),当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务。
异步任务按照执行先后顺序可以分为微任务和宏任务,分别存放于微任务队列和宏任务队列中,微任务先于宏任务执行。只有当微任务队列为空时才会执行宏任务,每次执行完一个宏任务后都要检查微任务队列是否为空,如果有微任务就要先执行所有微任务,否则才会继续执行宏任务。
微任务包括 promise回调函数(.then()/.catch()/.finally()) .nextTick()函数
宏任务包括 异步ajax请求 、定时器 setTimeout、setInterval等、文件操作等除微任务之外的异步任务。
2、事件循环
同步任务、异步任务执行过程:
①同步任务由JavaScript 主线程次序执行。②异步任务委托给宿主环境执行。③已完成的异步任务对应的回调函数,会被加入到任务队列中等待执行。④JavaScript 主线程的执行栈空闲时(无同步任务),会读取任务队列中的回调函数,次序执行。⑤JavaScript 主线程不断重复上面的第④ 步,该过程就是事件循环过程。
JavaScript 主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中依次执行。这个过程是循环不断的,所以整个的这种运行机制又称为EventLoop(事件循环)。
示例:
function showTbrw(val) {
console.log("同步任务" + val);
}
showTbrw(1);
new Promise((resolve, reject) => {
console.log("同步任务2---new Promise");
resolve(1);
}).then((val) => {
console.log("微任务1---promise.then");
}).finally(() => {
console.log("微任务2---promise.finally");
})
console.log("同步任务3");
setTimeout(() => {
console.log("宏任务1");
new Promise((resolve, reject) => {
console.log("同步任务5---new Promise");
resolve(1);
}).then((val) => {
console.log("微任务5---promise.then");
})
}, 500)
new Promise((resolve, reject) => {
console.log("同步任务4---new Promise");
resolve(1);
}).then((val) => {
console.log("微任务3---promise.then");
}).finally(() => {
console.log("微任务4---promise.finally");
})
setTimeout(() => {
console.log("宏任务11");
}, 0)
//输出结果
// 同步任务1
// 同步任务2---new Promise
// 同步任务3
// 同步任务4---new Promise
// 微任务1---promise.then
// 微任务3---promise.then
// 微任务2---promise.finally
// 微任务4---promise.finally
// 宏任务11
// 宏任务1
// 同步任务5---new Promise
// 微任务5---promise.then
从示例中可以看出:①同步任务先执行,同步执行完毕后才执行异步任务;②微任务优先于宏任务先执行,当微任务全部执行完毕后(此时微任务队列为空)才执行宏任务。③宏任务/微任务中包括同步任务或微任务/宏任务时,按照①-②的顺序继续执行.
示例执行过程:①代码全局上下文入栈;②执行并遇到showTbrw()函数,创建函数上下文并入栈;③执行showTbrw(),输出同步任务1,然后函数并出栈;④继续执行全局上下文,依次输出 同步任务2---new Promise、 同步任务3 、同步任务4---new Promise,至此同步任务执行完毕;⑤检查微任务队列,发现有四个微任务(then和finally),先将第一个微任务入栈、执行、出栈,然后依次执行剩下的微任务;⑥微任务队列为空时,宏任务队列中第一个宏任务入栈、执行并出栈,输出 宏任务11;⑦检查微任务队列还是为空,将第二个宏任务入栈、执行(输出宏任务1)、并出栈;⑧因为第⑦步中宏任务中包括同步任务和微任务,所以先执行同步任务,输出 同步任务5---new Promise;⑨检查微任务队列,微任务5入栈、执行并出栈,输出微任务5---promise.then;