事件循环
什么是javascript事件循环 (Event Loop)?
一个事件循环中有一个或者是多个任务队列
- 事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理
任务队列分为
macro-task(宏任务)与micro-task(微任务)
-
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
- 只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
事件循环原理
- JavaScript 事件循环是一种机制,它确保 JavaScript 程序不会“阻塞”,通过事件循环来实现异步, 即在执行一个任务时不会停止响应用户的输入。它通过不断执行下一个任务来实现这一目的。
- JavaScript 事件循环的工作原理是:当一个事件发生时,JavaScript 将它放入事件队列中。一旦当前任务完成,JavaScript 引擎就会从事件队列中取出下一个事件,并执行相应的回调函数。这个过程会一直重复,直到事件队列为空,然后 JavaScript 引擎就会停止执行。
简单来说,JavaScript 事件循环就是一个不断执行回调函数的循环,它确保 JavaScript 程序在执行任务时不会停止响应用户的输入。
因为 JavaScript 是一种单线程语言,所以如果一个任务执行时间过长,整个程序都会停止响应。这就是为什么 JavaScript 事件循环的工作原理非常重要,它确保了 JavaScript 程序不会“阻塞”,即在执行一个任务时也能响应用户的输入
事件循环实现
JavaScript 事件循环的实现方式因浏览器而异。大多数浏览器都采用了类似的实现方式,即使用一个“主循环”来不断执行下一个任务
- 在这种实现方式中,JavaScript 引擎会持续运行一个“主循环”,每当一个事件发生时,它就会把事件放入事件队列中。当主循环运行时,它会检查事件队列,如果事件队列中有事件,它就会执行相应的回调函数。这个过程会一直重复,直到事件队列为空,然后 JavaScript 引擎就会停止执行
简单来说,,JavaScript 事件循环就是一个“主循环”,它持续运行,不断执行下一个任务。当一个事件发生时,它会把事件放入事件队列中,并在主循环中执行相应的回调函数。这样,JavaScript 程序就能在执行任务时不断响应用户的输入
为什么js在浏览器中有事件循环的机制?
- 首先js是单线程的,如果同时需要执行多个操作
- 比如2个线程操作DOM,一个线程删除当前DOM节点,而另一个线程是操作当前DOM节点,那么这时候浏览器该听谁的, 对于浏览器而言就无法正确渲染出我们想要的结果,为了避免这个问题,就有了事件循环,来解决实现异步非阻塞
- 2个事件操作,有了事件循环,就可以判断这2个事件操作,谁先执行,看哪个操作是宏任务还是微任务
- 比如2个线程操作DOM,一个线程删除当前DOM节点,而另一个线程是操作当前DOM节点,那么这时候浏览器该听谁的, 对于浏览器而言就无法正确渲染出我们想要的结果,为了避免这个问题,就有了事件循环,来解决实现异步非阻塞
事件循环的理解
事件循环是一种机制,可以确保js程序不会被阻塞,可以解决异步非阻塞问题,可以确保单线程环境中能够高效地处理多个任务
- 1、首先所有的同步任务,会在主线程上执行,形成执行栈
- 2、主线程之外,还存在一个"任务队列",异步任务会放在任务队列里,存在多个异步任务的话,会进行排队等待
- 3、主线程执行栈的同步任务 执行完之后,才会去读取这个任务队列
- 4、这个过程不断重复,形成了一个循环,即事件循环
一个事件循环时间是多少?
这个时间是不确定的,根据浏览器的空闲时间 和 当前任务队列的事件情况来确定
异步和单线程
- 回调可以是同步也可以是异步,异步必须放在回调里执行
什么是异步(对比同步)
- 同步会阻塞代码执行,而异步不会
- 如果js不是异步的话,由于js代码本身是自上而下执行的,那么如果上一行代码需要执行很久,下面的代码就会被阻塞,对用户来说,就是”卡死”,这样的话,会造成很差的用户体验。
// 异步
console.log(100)
setTimeout(function() {
console.log(200)
},1000)
console.log(300)
// 同步
console.log(100)
alert(200) //同步会阻塞代码执行
console.log(300)
前端使用异步的场景
- 可能发生等待的时候
- 等待过程中不能像alert一样阻塞程序运行
- 因此,虽有的“等待情况”都需要异步
- 定时任务 setTimeout setInverval
- 网络请求:ajax请求 动态img标签加载
前端使用异步的场景有哪些
- 定时任务 setTimeout setInverval
- 网络请求:ajax请求 动态img标签加载
- 事件绑定 (因为有回调函数)
什么是单线程
- 一次只能执行一个任务
不能同时执行多个任务,比如一个杀毒软件,他能同时进行清理垃圾和扫描病毒等多种操作,就是多线程,可以执行多个任务
js是如何实现异步的
- 既然js是单线程的,那么js是如何实现异步的呢,是通过事件循环(event loop),理解了event loop 就理解了js的执行机制。
js为什么需要异步
如果js不是异步的话,由于js代码本身是自上而下执行的,那么如果上一行代码需要执行很久,下面的代码就会被阻塞,对用户来说,就是”卡死”,这样的话,会造成很差的用户体验。
宏任务和微任务
宏任务队列有:script(整体代码)、setTimeout、setInterval 、【事件绑定】、setImmediately,I/O,UI render
new promise
微任务队列有:async、await、promise、promise.then、promose.catch process.nexttick,Object.observe(已废弃了), MutationObserver(监听DOM树改变,html5新特性)
函数返回promise是属于微任务! 【如async返回promise,用await等待promise返回的结果 =>属于微任务】
- async是基于promise的,async 函数执行完毕会返回一个 promise,会放到微任务队列里面
ajax请求不属于宏任务,js线程遇到ajax请求,会将请求交给对应的http线程处理,一旦请求返回结果,就会将对应的回调放入宏任务队列,等请求完成执行。
注意:new promise() 创建promise对象会立即执行,它是同步的立即执行函数
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(()=>{
console.log('setTimeout');
},0)
async1();
new Promise((resolve)=>{
console.log('promise1');
resolve();
}).then(()=>{
console.log('promise2');
});
console.log('script end');
//结果
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
1、首先执行 script start
2、遇到事件,定时器放到下一个宏任务中
3、执行async1函数,输出async1 start
4、遇到 await async2(),【执行async2过程中会等待promise返回的结果,有结果之后才会执行下面的代码 (放到微任务中)】 ,输出async2
5、遇到new prmise()立即执行,输出promise1
6、script end 【一个主线程结束】
7、async1 end 【执行微任务里面的】
8、promise2 【执行下一个微任务】
9、setTimeout 【执行另一个宏任务】 (微任务执行完之后,执行宏任务里的定时器)
什么是宏任务队列,什么是微任务队列
在浏览器js引擎中,v8引擎: 有mircoTaskQueue 【微任务队列】、
taskQueue【任务=>理解为宏任务队列】
-
任务队列分为
taskQueue(宏任务)与microTaskQueue(微任务),在最新标准中,它们被分别称为task与jobs。 -
我们都知道javascript是单线程,只有唯一的事件循环。而javascript再执行过程中,除了函数调用栈来调节函数的执行顺序外,还有个任务队列task queue,任务队列可以是多个,其中又包括宏任务队列和微任务队列;
-
每一个宏任务执行完毕之后都必须将当前的微任务队列清空,才会执行下一个宏任务
- 宏任务队列一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务。
- 微任务队列中所有的任务都会被依次取出来执行,直到微任务队列为空。
- 在执行完所有的微任务之后,执行下一个宏任务之前,浏览器会执行 UI 渲染操作、更新界面。
第一个宏任务script进入后。从上往下执行,遇到同步代码就放在主线程形成执行栈中执行,异步的就注册异步任务,从上往下所有同步代码搞定后,也就是本次script宏任务执行完之后,就开始将微任务队列的任务一个一个取出来,也是从上到下执行当前微任务队列清空完之后,才会执行下一个宏任务
1、先执行主线程
2、然后执行任务队列 【分为微任务和宏任务】
3、每个宏任务执行完会把当前微任务全部执行完,才继续执行下一个宏任务
<script>
setTimeout(function(){
console.log('1')
});
new Promise(function(resolve){
console.log('2');
resolve();
}).then(function(){
console.log('3')
});
console.log('4');
结果:2->4->3>1
//1、遇到new Promise会立即执行,输出2
//2、输出4
//3、执行微任务 then 输出3
//4、执行宏任务 定时在 输出1
</script>
- js异步有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入eventqueue,然后在执行微任务,将微任务放入eventqueue,注意的是!!!!,这两个queue不是同一个queue
- 因为settimeout是宏任务,虽然先执行的他,但是他被放到了宏任务的eventqueue里面,然后代码继续往下检查看有没有微任务,检测到Promise的then函数把他放入了微任务序列。等到主线进程的所有代码执行结束后。先从微任务queue里拿回调函数,然后微任务queue空了后再从宏任务的queue拿函数
setTimeout(()=>{
new Promise((resolve)=>{
resolve()
}).then(()=>{
console.log('then')
})
setTimeout(()=>{
console.log('setTimeout 1')
},100)
},100)
结果:
then
setTimeout 1
script(整体代码(宏任务))是怎么入栈的
-
macro-task(宏任务)可以说作为主人,micro-task(微任务)作为奴隶
macro作为主导,它有支配micro的能力
在一个macro任务消灭之前,它会让它创建的micro任务都执行完,然后才进入执行下一个macro任务
- 所以奴隶先挂掉的,最后才到主人挂掉。。
为什么有了宏任务,还需要微任务? 只有一种任务可以吗
- 因为宏任务是放在队列里都保持这先进先出原则执行
如果只有宏任务,如果这时候有紧急的任务需要优先去处理,这时候只有宏任务是不够的,因为宏任务是先进先出的原则执行,所以就引入了微任务的概念
- 每一个宏任务执行完毕之后都必须将当前的微任务队列清空,才会执行下一个宏任务
- 在浏览器里,浏览器先执行一遍宏任务,就去微任务队列里,把所有微任务执行,才执行下一个宏任务
为什么微任务比宏任务执行更早?
- 根本原因是 :微任务的优先级高于宏任务
比如要处理紧急任务,只有宏任务是不够的,就引入了微任务的概念,让微任务执行完之后再执行下一个宏任务
- 执行EventLoop时候,会优先去检查微任务队列,清空之后再去检查宏任务队列
EventLoop例子
console.log('start');
setTimeout(() => {
console.log('timeout');
});
Promise.resolve().then(() => {
console.log('resolve');
});
console.log('end');
我们来分析一下:
- 刚开始整个脚本作为一个宏任务来执行,对于同步代码直接压入执行栈(关于执行栈,若不了解请移步之前的文章《JavaScript内存机制之问——数据是如何存储的?》)进行执行,因此先打印start和end
- setTimeout 作为一个宏任务放入宏任务队列
- Promise.then作为一个为微任务放入到微任务队列
- 当本次宏任务执行完,检查微任务队列,发现一个Promise.then, 执行
- 接下来进入到下一个宏任务——setTimeout, 执行
第一个宏任务script执行之后,然后执行微任务,然后到下一个宏任务setTimeout
因此最后的顺序是:
start
end
resolve
timeout
1、一开始整段脚本作为第一个宏任务执行
2、执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
3、当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空
4、执行浏览器 UI 线程的渲染工作
5、检查是否有Web worker任务,有则执行
6、执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
new Promise(resolve => {
console.log('promise')
setTimeout(() => {
console.log(1)
resolve()
console.log(2)
}, 1000)
}).then(res => {
console.log('then')
})
console.log('script')
// promise => script => 1 => 2 =>then
- 1、遇到new Promise会立即执行,输出promise
- 2、输出同步任务代码 输出script
- 3、同步执行完之后,查看微任务,发现then,执行微任务的then
- 4、由于微任务里面then嵌套了定时器,需要先执行宏任务的定时器,才能执行then
- 因此输出1、2,最后才执行微任务里的then
目前还有宏任务吗 ?
-
其实宏任务一直是没有的,为了方便理解,与这个微任务队列区分,就把taskQueue理解为宏任务队列
-
v8引擎: 有mircoTaskQueue 【微任务队列】
-
宿主环境:
taskQueue【任务=>理解为宏任务队列】 -
V8 引擎内部会维护一个微任务队列(Microtask Queue),用于存放 JavaScript 引擎内部产生的微任务,比如 Promise 的回调函数、MutationObserver 的回调函数等。微任务会在当前宏任务执行完毕后立即执行,这保证了微任务能够优先于下一个宏任务执行
-
为了区分这两种任务队列,我们通常将宿主环境中的任务队列称为宏任务队列 , 将 JavaScript 引擎内部的任务队列称为微任务队列
V8 引擎只负责执行 JavaScript 代码,而异步任务的管理则由宿主环境来完成
- 宿主环境是指JavaScript引擎运行的宿主环境
- 宿主环境提供了JavaScript引擎运行所需的各种环境和API,包括DOM操作、网络请求、文件系统访问等
- 比如nodejs 是v8引擎运行的js的环境
promise执行顺序
Promise.resolve()
.then(() => {
console.log(0);
return Promise.resolve(4); // 1、promise需要由pediing 变成 fulfilled 2、then 追加到微任务
})
.then((res) => {
console.log(res);
});
Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
})
.then(() => {
console.log(6);
});
// 输出 0 、1 、2 、3、 4、 5、 6
解析执行顺序
then交替执行问题
- 1、当多个fulfilled promise状态,同时执行then链式调用
- 即多个Promise.resolve()的时候,也就是多个fulfilled状态
- 2、then会交替执行
- 3、这是编译器的优化,防止一个promise占据太久时间
then返回promise实例问题
-
会出现慢2拍的效果 (会延迟2步执行)
- 第一拍: promise需要由pediing 变成 fulfilled
- 第二拍: then函数 追加到微任务队列中
-
相当于多出一个promise实例
-
也会遵循 交替执行