小试牛刀之宏任务和微任务

184 阅读6分钟

上节讲了宏任务、微任务的概念,本篇文章通过几个例子来加深宏任务和微任务的理解

案例一:async + await

先上代码,大家看看这段代码最终输出的结果顺序是怎么样的?

async function async1() {
  console.log("async1 start");
  await async2();
  //这一步是本案例的重点,await后面的代码会阻塞,类似于传入.then()中的回调
  console.log("async1 end");
}
async function async2() {
  console.log("async2"); 
}
// new Promise(function(resolve,reject) {
//   resolve(undefined);
// })
console.log("main start")
setTimeout(function() {
  console.log("setTimeout");
})
async1();
new Promise(function(resolve) {
  console.log("promise 构造");
  resolve();
}).then(function() {
  console.log("promise then");
})

运行结果:

image.png

过程分析:

  1. 代码从上往下看,async1async2只定义未执行,先不用管。第 13 行是同步代码,所以先输出 "main start"

  2. 第 14 行 setTimeout 在一定时间后会产生宏任务,因为没有定义时间参数,所以每个浏览器的最低执行时常可能会不一致,chrome浏览器大概在 1ms 左右。此时,宏任务队列等待输出:"setTimeout"

  3. 第 17 行 async1 执行,同步代码输出 "async1 start"

  4. 第 3 行 async2 被调用,同步代码输出 "async2";需要注意一点,async2 函数没有进行任何操作,默认返回 'undefined',它会被包裹成类似于 10 ~12 行代码的样子。

  5. 第 5 行代码在 await async2(); 之后执行,await后面的代码会被阻塞 ,类似于传入then()中的回调,变成异步微任务。此时,微任务队列等待输出:"async1 end"

  6. 第 19 行是同步代码,输出 "promise 构造"

  7. 第 20 行,resolve后执行 .then() 中的回调,此时,微任务队列等待输出:"async1 end"、"promise then"

    同步代码按照执行顺序依次输出,异步代码按照微任务比宏任务优先执行的顺序输出,最终得到以上输出结果。

案例二:宏任务产生微任务

console.log("main start");

setTimeout(function setTimeout0() {
  console.log("T1:宏任务");
  Promise.resolve().then(()=>{
    console.log("T2:微任务");
  })
})

new Promise(function(resolve,reject) {
  console.log("T3:Promise 构造");
  setTimeout(function setTimeout300() {
    console.log("T4:宏任务");
    resolve("T6");
    Promise.resolve().then(()=>{
      console.log("T5:微任务");
    });
  },300)
}).then((res)=>{
  console.log("T6:微任务");
})

运行结果:

image.png

过程分析:

  1. 第 1 行代码毋庸置疑,直接输出 "main start"
  2. 第 3 行,在 0~1ms 后setTimeout0 会被放入宏任务队列;
  3. 第 10 行,new Promise 中优先输出同步代码 "T3:Promise 构造"
  4. 第 12 行,在 300ms 后setTimeout300 会被放入宏任务队列;
  5. 等待 0~1ms 后,setTimeout0进入宏任务队列并开始执行,首先输出 "T1:宏任务";下一步,Promise.resolve()后,将 console.log("T2:微任务") 放入微任务队列首位,因此接着输出 "T2:微任务"setTimeout0执行完毕;
  6. 等待 300ms 后,setTimeout300进入宏任务队列并开始执行,首先输出 "T4:宏任务"
  7. 第 14 行 resolve 后会马上产生一个微任务,于是第 20 行代码被立刻放入微任务队列;下一步,执行 Promise.resolve() ,第 16 行代码也被放入微任务队列;清空微任务队列,依次输出 "T6:微任务"、"T5:微任务"setTimeout300执行完毕。

案例三:MessageChannel 的优先级

image.png

MessageChannel API允许我们创建一个新的消息通道,并通过它的两个 MessagePort属性 (port1、port2)发送数据。

使用示例

image.png

验证 MessageChannel 的执行优先级

MessageChannelDOM Event的形式发送消息,所以它属于异步的宏任务。

查阅资料发现宏任务是有优先级的,DOM 事件的优先级比 timer 的优先级高,MessageChannel 的 message 事件属于DOM事件,所以暂且可以得出一个结论:

MessageChannel执行优先级大于setTimeout

通过代码来验证一番:

<body>
  <button id="startMsg">BroadcastChannel 发送广播</button>
  <script>
    startMsg.onclick = function() {
      console.log("main");
      setTimeout(()=>{
        console.log("setTimeout 宏任务 1");
      });
      new Promise((resolve,reject)=>{
        console.log("promise 构造");
        resolve(5);
      }).then((data)=>{
        console.log("promise 微任务", data);
      });
      const ch = new MessageChannel();
      ch.port1.onmessage = function(ev) {
        console.log("收到  MessageChannel 消息", ev.data.msg);
      };
      ch.port2.postMessage ( {msg:"11"});
      new Promise((resolve,reject)=>{
        console.log("promise2 构造");
        resolve(6);
      }).then((data)=>{
        console.log("promise2 微任务", data);
      });
      setTimeout(()=>{
        console.log("setTimeout 宏任务 2");
      });
      console.log("main end");
    }
  </script>
</body>

运行结果:

image.png

我们从结果中发现,MessageChannel 在微任务之后执行,它确实是异步任务,但是它并没有在 srtTimeout 之前执行,这是为什么呢?

MessageChannel 表现出的优先级可能会和 timer 相同

看看 聊聊浏览器宏任务的优先级 这篇文章,通过以下的结论,大概能够解释上面的问题:

  1. 根据宏任务的优先级规范可知,MessageChannel确实先于setTimeout执行;
  2. 浏览器实现的自由度以及防饥渴机制也有可能带来宏任务优先级的不稳定;
  3. 当前大多数 chrome 版本测得,MessageChannel和timer具有相同的优先级;

案例四:Promise.then

第四个案例,重点在于 Promise - then参数回调函数返回值为Promise对象时会产生两次微任务

先看普通版的 Promise.then 案例:

//第一个 Promise
Promise.resolve()
  .then(()=>{
    console.log(1)
  })
  .then(()=>{
    console.log(3)
    //return Promise.resolve(7);
  })
  .then((res)=>{
    console.log(res);
  });

//第二个 Promise
Promise.resolve()
  .then(()=>{
    console.log(2);
  })
  .then(()=>{
    console.log(4);
  })
  .then(()=>{
    console.log(5);
  })
  .then(()=>{
    console.log(6);
  })
  .then(()=>{
    console.log(8);
  })

运行结果:

image.png

过程分析:

  1. Promise 是同步代码,resolve() 之后产生微任务,第 4 行代码的 1 进入微任务队列,此时还不能输出 1 ,因为下面还有第 2 个同步的 Promise;
  2. 自上而下执行第二个 Promise,第 17 行代码的 2 进入微任务队列;
  3. 此时同步代码执行完毕,清空微任务队列,输出 1 ,输出 1 之后又产生了一个微任务,第 7 行代码的 3 进入微任务队列;
  4. 按照顺序清空微任务队列,输出 2,输出 2 之后又产生了一个微任务,第 20 行代码的 4 进入微任务队列;
  5. 依此类推,输出 3 之后 res 进入微任务队列,res 的值为 undefined;
  6. 按照上述规律,依此输出微任务队列的值,就得到上述的结果。

加点难度

如果将上面第 8 行的代码注释取消,会发生什么情况呢?

运行结果:

image.png

从结果分析,发现 7 好像滞留了两次后才进入微任务队列。这种现象涉及到一些协议以及和V8引擎执行顺序有关,大家只做了解即可,记住一个结论:

then方法参数回调函数返回Promise对象是会开启两次微任务

以后遇到类似的题目,都可以套用该结论推理解决。

该结论的详细的验证过程可以参考 这篇文章

下期再见