宏任务、微任务到底是什么关系(事件循环)

115 阅读5分钟

浏览器的eventLoop在面试的时候总是容易被问到,到底什么是微任务,什么是宏任务,eventLoop到底是怎么loop的? MDN给出了这样的解释:

起初微任务和任务之间的差异看起来不大。它们很相似;都由位于某个队列的 JavaScript 代码组成并在合适的时候运行。但是,只有在迭代开始时队列中存在的任务才会被事件循环一个接一个地运行,这和处理微任务队列是殊为不同的。

有两点关键的区别。

首先,每当一个任务存在,事件循环都会检查该任务是否正把控制权交给其他 JavaScript 代码。如若不然,事件循环就会运行微任务队列中的所有微任务。接下来微任务循环会在事件循环的每次迭代中被处理多次,包括处理完事件和其他回调之后。

其次,如果一个微任务通过调用 queueMicrotask(), 向队列中加入了更多的微任务,则那些新加入的微任务 会早于下一个任务运行。这是因为事件循环会持续调用微任务直至队列中没有留存的,即使是在有更多微任务持续被加入的情况下。

警告:因为微任务自身可以入列更多的微任务,且事件循环会持续处理微任务直至队列为空,那么就存在一种使得事件循环无尽处理微任务的真实风险。如何处理递归增加微任务是要谨慎而行的。

来看看testEventLoop函数,调用之后,会是什么样的输出。

function testEventLoop () {
  console.log('start'); // 直接执行

  const promise1 = new Promise((resolve, reject) => {
    console.log('create promise.'); // 直接执行
    resolve('resolve promise 1'); 
  });

  setTimeout(() => {
    // setTimeout中的所有代码压入宏务队列中
    console.log('第一个setTimeout开始执行');
    const nextEventLoopStartPromise = new Promise((resolve, reject) => {
      console.log('第一个setTimeout执行中创建promise');
      resolve('resolve第一个setTimeout中的promise');
    });
    nextEventLoopStartPromise.then((res) => {
      console.log('第一个setTimeout中的promise被执行');
    });
    console.log('第一个setTimeout结束');
    setTimeout(() => {
      console.log('第一个setTimeout中创建的setTimeout被执行');
    }, 0);
  }, 0);

  const promise2 = new Promise((resolve, reject) => {
    console.log('create promise2 in setTimeout'); // 执行
    resolve('resolve promise2 in setTimeout'); 
  });

  console.log('创建列2个promise和一个setTimeout后的console'); // 直接执行

  // 执行promise,then,then的回调被压入微任务队列 微任务队列+1
  promise2.then((res) => {
    console.log('promise2的then被执行', res); // 微任务队列获得控制权后,微任务队列-1
  });

  // 执行setTimeout,宏任务队列+1
  setTimeout(() => {
    console.log('第二个setTimeout被调用'); // 宏任务队列获得控制权后,宏任务队列-1
  }, 0);

  const promise3 = new Promise((resolve, reject) => {
    console.log('create promise3'); // 执行
    resolve('resolve promise3 '); // 生成当前promise then回掉
  });

  // 执行promise,then,then的回调被压入微任务队列 微任务队列+1
  promise3.then((res) => {
    console.log('promise3的then被执行', res); // 微任务队列获得控制权后,微任务队列-1
  });

  // 执行promise,then,then的回调被压入微任务队列 微任务队列+1
  promise1.then((res) => {
    console.log('promise1的then被执行', res); // 微任务队列获得控制权后,微任务队列-1
  });

  console.log('end'); // 执行
}

testEventLoop()

执行testEventLoop之后,会进行2次事件循环

第一次,

(1)执行完所有的同步任务(promise的创建也是同步任务)。并将当前同步任务产生的微任务和宏任务分别入队,最终结果如下:
microArray = [promise2.then, promise3.then, promise1.then]
macroArray = [setTimeout1.callback, setTimeout2.callback]
(2)执行微任务队列中所有的微任务,执行结束后
microArray = []
(3)微任务执行完毕,从宏任务队列中取一个宏任务进行执行,执行结束后
macroArray = [setTimeout2.calback]

这个时候,就进入第二次循环了

(1)执行setTimout1中的回调中的代码,结束后
microArray = [nextEventLoopStartPromise.then]
macroArray = [setTimeout2.callback,setTimeout1中的setTimeout.callback]
(2)执行微任务队列中所有的任务,后microArray = []
(3)执行宏任务中的setTimeout2,后macroArray = [setTimeout1中的setTimeout.callback]

第三次循环
(1)没有同步任务
(2)没有微任务
(3)执行setTimeout1中的setTimeout.callback

结束
microArray = [] macroArray = []

因此总结一下事件流

flowchart
B[执行同步任务]
B --> C[执行所有的微任务]
C --> D[执行一个宏任务]
D --> B

此时,不禁要疑问了,如果在promise的回调中不停的增加promise,会发生什么?看MDN的解释,应该是会直接死循环,深度递归出不来了?? 真的会这样吗?
话不多说,上代码

function testInfinityPromise () {
  console.log('开始作死旅程');
  const promise1 = new Promise((resolve, reject) => {
    const inerPromise = new Promise((res, reject) => {
      res('在promise中resolve的new promise');
    });
    resolve(inerPromise);
  });
  promise1.then((res) => {
    console.log(res);
    // 外层循环
    for (let i = 0; i < 100; i++) {
      const p = new Promise((r, j) => {
        console.log('第一个微任务中的promise被建立');
        r(33); // 微任务队列+1
        setTimeout(() => {
          console.log('微任务中建立的宏任务');   
          // 宏任务队列+1,此时的宏任务队列 [setTimouet1,...,setTimout(i)]
        });
      });
      p.then((r) => {
        // 微任务队列-1
        console.log('第一个微任务中的微任务被执行');
        // 内循环,这里循环100次,外层循环每循环一次
        for (let i = 0; i < 100; i++) {
          const p = new Promise((r, j) => {
            console.log('第二个循环中的promise建立 ');
            r(33); // 微任务队列+1
          });
          p.then((r) => {
            console.log('第二个循环中的promise被执行'); // 微任务队列-1(异步)
          });
        }
        // 内循环结束,微任务队列+100
      });
    } // 外循环结束,微任务队列10000
  });

// 此时的微任务队列剩下还没有被执行的10000个微任务
  setTimeout(() => {
    console.log('setTimeout'); // 宏任务队列+1
  }, 0);
}

testInfinityPromise();

将以上代码复制到浏览器运行,发现在promise中添加到微任务队列的任务会在timeout执行之前全部执行。说明无论宏任务与微任务是什么时候创建的,只要它进入队列,就会遵循事件循环机制,直到微任务队列中的任务为空为止才会执行宏任务队列中的任务。

当然了,应该不会有人用promise递归,不停的创建promise吧,不会吧不会吧,代码写起来不累吗?

这时候,面试官可能还会问,常见的宏任务有哪些,常见的微任务有哪些?

常见宏任务:
setTimeout
setInterval

常见微任务:
promise

写到这里,不得不再讲一下:如果promise不被resolve会发生什么

requestAnimationFrame与requeatIdelCallback有什么区别