一文搞懂javascript事件循环原理?「前端每日一题v22.11.16」
了解javascript的第一步,就是要了解事件循环机制。但是要真正的了解javascript的事件循环机制并不容易,因为它是javascript引擎最基础的部分。它可以让单线程的javascript以非阻塞方式执行
事件循环机制Event Loop,要真正了解这些,我们可能需要知道javascript引擎相关内容,比如调用栈,宏任务,微任务,任务队列等相关,一起看一下这些概念
调用栈
调用栈是一种跟踪javascript代码执行的数据结构,它是一个栈,因此遵循先进后出的数据结构,执行的每个函数都表示为调用栈中的一个帧,并放在前一个函数的顶部
举个例子,这是最常见的一段javascript代码
function foo(){
console.log('foo')
bar()
}
function bar(){
console.log('bar')
}
foo()
我们详细的讲述一下上述代码的执行流程
- 调用栈最初是空的,忽略前面所有的函数,直到foo被调用
- 函数foo被压入调用栈,调用栈列表为foo
- 执行foo中的代码console.log,栈列表添加console.log函数,这个函数执行完就会立刻被推出栈,所以栈列表还是foo
- 代码执行到bar时候,该函数被调用
- bar函数被压入调用栈,调用栈列表为bar-foo
- 执行bar中的代码,直到全部执行完毕,调用栈列表没变
- 返回来继续执行foo函数中bar之后的代码
- 删除调用栈列表中bar函数,调用栈列表为foo
- foo中的bar函数后续没有可执行代码
- 删除调用栈列表中的foo,此时调用栈列表为空
一开始我们会往空的调用栈中推入函数,执行完函数代码之后,调用栈又会移除这个函数,最终又会得到一个空的调用栈,如果在执行过程中,
任务和任务队列
上面的案例主要是针对javascript单线程的执行方式,但是这种方式是不可取的,比如如果我们碰到了定时器,或者请求数据等执行时间比较长的代码的话,后面的逻辑就得等这些代码执行完成才能进行下一步
所以就有了任务队列的概念
任务分为同步任务和异步任务,区别就是同步任务执行后立刻就能取到结果,异步任务需要等一会儿才能取到结果
同步任务执行按照调用栈执行逻辑,执行完成移出调用栈,异步任务执行也会进入调用栈,不同的是它会将其回调函数或者回调任务放入一个任务队列,任务队列遵循先进先出的原则,放入任务队列的函数不会立刻执行,需要等待调用栈中同步的任务执行完成
当调用栈清空之后,也就是所有同步任务结束之后,解释器开始读取任务队列执行,将已经完成的异步任务放入调用栈执行
看代码
console.log('foo')
setTimeout(() => {
console.log('bar')
}, 1000)
console.log('loo')
- console.log('foo')压入调用栈,执行完成推出调用栈
- setTimeout压入调用栈
- 浏览器的定时线程会等时间结束后将setTimeout中的回调函数箭头函数放入任务队列
- setTimeout推出调用栈
- console.log('loo')压入调用栈,执行并推出
- 此时同步任务全部执行完成,调用栈为空
- 1s过后,浏览器的定时线程将匿名函数放入任务队列
- 调用栈为空,执行任务队列匿名函数内容
- console.log('bar')执行
- 匿名函数推出调用栈
- 调用栈为空
宏任务和微任务
前面说到的任务队列,其实还分为宏任务和微任务,这两种都属于异步任务,它们的区别就在于它们的执行顺序。
为什么会区分宏任务和微任务,最主要的一点就是如果任务队列中某一个任务需要1个小时,后续的任务都需要等待一个小时,这显然是不合理的,所以宏任务和微任务最主要的一个目的就是区分任务的优先级,微任务优先级高于宏任务
宏任务
- 整体代码都属于宏任务
- UI交互事件
- I/O
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame
微任务
- process.nextTick
- MutationObserver
- Promise.then catch finally
console.log('foo')
setTimeout(() => {
console.log('bar')
}, 0)
new Promise((res) => {res(1)}).then((d) => {console.log(d)})
顺序为foo、1、bar
事件循环
上面的内容结合起来,基本上就是整个事件循环的机制了
- 先从调用栈开始
- 同步任务压入调用栈,开始执行
- 碰到异步任务,将有结果的回调压入任务队列,注意区分宏任务和微任务
- 调用栈清空之后,微任务队列按照先入先执行顺序开始执行
- 微任务队列清空之后,一个循环结束
- 进入宏任务队列直到宏任务队列清空
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0)
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3');
});
console.log('script end');
按照以上逻辑,做完这个题就可以出师了,以后碰到所有的这种执行顺序的都不怕了
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。