头图

一文搞懂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()

我们详细的讲述一下上述代码的执行流程

  1. 调用栈最初是空的,忽略前面所有的函数,直到foo被调用
  2. 函数foo被压入调用栈,调用栈列表为foo
  3. 执行foo中的代码console.log,栈列表添加console.log函数,这个函数执行完就会立刻被推出栈,所以栈列表还是foo
  4. 代码执行到bar时候,该函数被调用
  5. bar函数被压入调用栈,调用栈列表为bar-foo
  6. 执行bar中的代码,直到全部执行完毕,调用栈列表没变
  7. 返回来继续执行foo函数中bar之后的代码
  8. 删除调用栈列表中bar函数,调用栈列表为foo
  9. foo中的bar函数后续没有可执行代码
  10. 删除调用栈列表中的foo,此时调用栈列表为空

一开始我们会往空的调用栈中推入函数,执行完函数代码之后,调用栈又会移除这个函数,最终又会得到一个空的调用栈,如果在执行过程中,

任务和任务队列

上面的案例主要是针对javascript单线程的执行方式,但是这种方式是不可取的,比如如果我们碰到了定时器,或者请求数据等执行时间比较长的代码的话,后面的逻辑就得等这些代码执行完成才能进行下一步

所以就有了任务队列的概念

任务分为同步任务和异步任务,区别就是同步任务执行后立刻就能取到结果,异步任务需要等一会儿才能取到结果

同步任务执行按照调用栈执行逻辑,执行完成移出调用栈,异步任务执行也会进入调用栈,不同的是它会将其回调函数或者回调任务放入一个任务队列,任务队列遵循先进先出的原则,放入任务队列的函数不会立刻执行,需要等待调用栈中同步的任务执行完成

当调用栈清空之后,也就是所有同步任务结束之后,解释器开始读取任务队列执行,将已经完成的异步任务放入调用栈执行

看代码

console.log('foo')

setTimeout(() => {
  console.log('bar')
}, 1000)

console.log('loo')
  1. console.log('foo')压入调用栈,执行完成推出调用栈
  2. setTimeout压入调用栈
  3. 浏览器的定时线程会等时间结束后将setTimeout中的回调函数箭头函数放入任务队列
  4. setTimeout推出调用栈
  5. console.log('loo')压入调用栈,执行并推出
  6. 此时同步任务全部执行完成,调用栈为空
  7. 1s过后,浏览器的定时线程将匿名函数放入任务队列
  8. 调用栈为空,执行任务队列匿名函数内容
  9. console.log('bar')执行
  10. 匿名函数推出调用栈
  11. 调用栈为空

宏任务和微任务

前面说到的任务队列,其实还分为宏任务和微任务,这两种都属于异步任务,它们的区别就在于它们的执行顺序。

为什么会区分宏任务和微任务,最主要的一点就是如果任务队列中某一个任务需要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

事件循环

上面的内容结合起来,基本上就是整个事件循环的机制了

  1. 先从调用栈开始
  2. 同步任务压入调用栈,开始执行
  3. 碰到异步任务,将有结果的回调压入任务队列,注意区分宏任务和微任务
  4. 调用栈清空之后,微任务队列按照先入先执行顺序开始执行
  5. 微任务队列清空之后,一个循环结束
  6. 进入宏任务队列直到宏任务队列清空
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');

按照以上逻辑,做完这个题就可以出师了,以后碰到所有的这种执行顺序的都不怕了


FE情报局
30 声望5 粉丝