这一次,彻底搞懂 JS 事件循环(eventLoop)

223 阅读5分钟

看了很多关于 JS 运行机制的文章,每次看的时候都会,但是没过几天又忘了,还是自己理一遍,下次如果还忘,就方便回顾了。

JS 事件循环

先来看一张图:

excution.png

首先,JS 有同步和异步任务,其中同步是在主线程执行的,执行时会生成一个执行栈

同时,有一个任务队列,里面主要放异步任务的事件回调的。

如果执行栈中的同步任务执行完毕,这时执行栈为空,系统就会去看看任务队列中有没有要执行的事件,如果有,就加到执行栈中开始执行

看到这里,新手可能比较懵,没事,后面有例子,看完后面例子再来看这几句话就懂了。

宏任务和微任务

在 JS 中常见的任务如下:

宏任务

  • script 中的代码块
  • setTimeout
  • setInterval
  • setImmediate()-Node
  • requestAnimationFrame()-浏览器

微任务

  • Promise.then()
  • await 后面的代码
  • catch
  • finally
  • process.nextTick()-Node
  • Object.observe
  • MutationObserver

看下面的图:

eventLoop.png

首先执行一个宏任务,执行结束后判断是否存在微任务。

有微任务先执行所有的微任务,再渲染,没有微任务则直接渲染。

然后再接着执行下一个宏任务。

下图为完整的事件循环图:

allEventLoop.png

常见的事件循环面试题

前面说这么多没有多大用,看几个例子就明白了:

1.下面代码输出什么

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();
}).then(function () {
  console.log('promise2');
});
console.log('script end');
  1. 首先代码从上往下执行,输出script start
  2. 遇到setTimeout,将其放到任务队列的宏任务中。
  3. 执行async1,输出async1 start,执行await async2(),输出async2,此时await async2()后面的代码不执行,而是将其放入到微任务中。
  4. 代码继续执行,执行到new Promise,输出promise1,执行resolve()后将.then()放到微任务中。
  5. 往下执行,输出script end
  6. 这时,第一个script宏任务执行完毕,我们来看看在任务队列中剩下哪些宏任务和微任务,首先有一个console.log('setTimeout')宏任务,和两个微任务,一个先被放入任务队列的console.log('async1 end'),另一个是后被放入的console.log('promise2')
  7. 根据前几张图,我们知道,现在肯定执行微任务,那谁先被放入的就先执行谁,所以输出async1 end,然后输出promise2
  8. 至此,因为任务队列中已没有微任务,第一轮事件循环执行完毕。
  9. 接着再看看任务队列中有没有宏任务,有的,输出setTimeout,此时,所有任务执行完毕,退出程序。 最后的执行结果如下:
  • script start
  • async1 start
  • async2
  • promise1
  • script end
  • async1 end
  • promise2
  • setTimeout

2.下面代码输出什么

console.log('start');
setTimeout(() => {
  console.log('children2');
  Promise.resolve().then(() => {
    console.log('children3');
  });
}, 0);

new Promise(function (resolve, reject) {
  console.log('children4');
  setTimeout(function () {
    console.log('children5');
    resolve('children6');
  }, 0);
}).then(res => {
  console.log('children7');
  setTimeout(() => {
    console.log(res);
  }, 0);
});
  1. 首先不用说,肯定先输出start
  2. 遇到第一个setTimeout(我们暂且称他为setTimeout1吧),由于setTimeout1是宏任务,所以放到任务队列中的宏任务当中。
  3. 遇到new Promise,执行,输出children4,遇到第二个setTimeout(我们暂且称他为setTimeout2吧),所以也把他放到任务队列中的宏任务当中。
  4. 由于这个现在还没有执行resolve,所以现在微任务中还是空的,也就是还没有then
  5. 此时,因为任务队列中已没有微任务,第一轮事件循环执行完毕。
  6. 执行setTimeout1,输出children2,同时,出现一个then微任务,将其放到任务队列中的微任务当中。
  7. setTimeout1执行完毕,看看任务队列中有没有微任务,有的,输出children3,本次事件循环结束。
  8. 执行setTimeout2,输出children5,然后resolve('children6'),将then放到任务队列中的微任务当中。
  9. setTimeout2执行完毕,看看任务队列中有没有微任务,有的,输出children7,然后又遇到了一个setTimeout,将其放到任务队列中的宏任务当中,本次事件循环结束。
  10. JS 再检查任务队列中还有没有任务,发现还有一个,拿出来执行,然后输出children6,此时,所有任务执行完毕,退出程序。
    最后的执行结果如下:
  • start
  • children4
  • children2
  • children3
  • children5
  • children7
  • children6
    如果此时我把第一个setTimeout的时间设置为 1000,结果会是什么呢?大家可以去试一试。

3.下面代码输出什么

const p = function () {
  return new Promise((resolve, reject) => {
    const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(1);
      }, 0);
      resolve(2);
    });
    p1.then(res => {
      console.log(res);
    });
    console.log(3);
    resolve(4);
  });
};
p().then(res => {
  console.log(res);
});
console.log('end');
  1. 执行代码,Promise本身是同步的立即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到p1.then会先放到微任务队列中,接着往下执行,输出3
  2. 遇到p().then会先放到微任务队列中,接着往下执行,输出end
  3. 宏任务执行完成后,开始执行微任务队列中的任务,首先执行p1.then,输出2, 接着执行p().then, 输出4
  4. 微任务执行完成后,开始执行宏任务,setTimeout, resolve(1),但是此时p1.then已经执行完成,此时1不会输出。 最后的执行结果如下:
  • 3
  • end
  • 2
  • 4

如果将上方的resolve(2)删掉,结果输出什么呢?大家可以去试一试,此时输出结果为:3 end 4 1