阅读 276

宏任务和微任务

首先,必须先知道JS运行机制。

JS运行机制

JS是单线程

”JS是单线程的”指的是JS 引擎线程。

在浏览器环境中,有JS 引擎线程和渲染线程,且两个线程互斥。 Node环境中,只有JS 线程。

宿主

JS运行的环境。一般为浏览器或者Node。

执行栈

是一个存储函数调用的 栈结构,遵循先进后出的原则。

function foo() {
  throw new Error('error')
}
function bar() {
  foo()
}
bar()
复制代码

当开始执行 JS 代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,在图中我们也可以发现,foo 函数后执行,当执行完毕后就从栈中弹出了。

Event Loop

image.png

JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它。 也就是等待宿主环境分配宏观任务,反复等待 - 执行即为事件循环。

Event Loop中,每一次循环称为tick,每一次tick的任务如下:

  • 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
  • 检查是否存在微任务,有则会执行至微任务队列为空;
  • 如果宿主为浏览器,可能会渲染页面;
  • 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)

宏任务和微任务

ES6 规范中,microtask 称为 jobs,macrotask 称为 task 宏任务是由宿主发起的,而微任务由JavaScript自身发起。

image.png

常见宏任务和微任务面试题

async 和 await

代码

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end')
}
async1()
复制代码

执行结果

// async2 end
// async1 end
复制代码

调用 async1 函数,遇到 await, 让出进程,等待后面表达式 async2() 执行完异步操作,在执行 await 后面的代码。

改为ES5的写法:

new Promise((resolve, reject) => {
  // console.log('async2 end')
  async2() 
  ...
}).then(() => {
 // 执行async1()函数await之后的语句
  console.log('async1 end')
})
复制代码

setImmediate 和 setTimeout

setImmediate和process.nextTick为Node环境下常用的方法(IE11支持setImmediate),所以,后续的分析都基于Node宿主。

Node.js是运行在服务端的js,虽然用到也是V8引擎,但由于服务目的和环境不同,导致了它的API与原生JS有些区别,其Event Loop还要处理一些I/O,比如新的网络连接等,所以与浏览器Event Loop不太一样。

执行顺序如下:

timers: 执行setTimeout和setInterval的回调
pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
idle, prepare: 仅系统内部使用
poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎>所有的异步都在这个阶段处理。
check: setImmediate在这里执行
close callbacks: 一些关闭的回调函数,如:socket.on('close', ...)

一般来说,setImmediate会在setTimeout之前执行,如下:

console.log('outer');
setTimeout(() => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
  });
}, 0);
复制代码

其执行顺序为:

  1. 外层是一个setTimeout,所以执行它的回调的时候已经在 timers 阶段了
  2. 处理里面的setTimeout,因为本次循环的timers正在执行,所以其回调其实加到了下个timers阶段
  3. 处理里面的setImmediate,将它的回调加入 check 阶段的队列
  4. 外层timers阶段执行完,进入pending callbacks,idle, prepare,poll,这几个队列都是空的,所以继续往下
  5. 到了check阶段,发现了setImmediate的回调,拿出来执行
  6. 然后是close callbacks,队列是空的,跳过
  7. 又是timers阶段,执行console.log('setTimeout')

但是,如果当前执行环境不是timers阶段,就不一定了

Node里面对setTimeout的特殊处理:setTimeout(fn, 0)会被强制改为setTimeout(fn, 1)。

setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});
复制代码

其执行顺序为:

  1. 遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times阶段
  2. 遇到setImmediate塞入check阶段
  3. 同步代码执行完毕,进入Event Loop
  4. 先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过
  5. 跳过空的阶段,进入check阶段,执行setImmediate回调

Promise 和 process.nextTick

因为process.nextTick为Node环境下的方法,所以后续的分析依旧基于Node

process.nextTick() 是一个特殊的异步API,其不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。

所以,nextTick和Promise同时出现时,肯定是nextTick先执行,原因是nextTick的队列比Promise队列优先级更高。

Vue的$nextTick

vm.$nextTick 接受一个回调函数作为参数,用于将回调延迟到下次DOM更新周期之后执行。

因为微任务优先级太高,Vue 2.4版本之后,提供了强制使用宏任务的方法。

vm.$nextTick优先使用Promise,创建微任务。

如果不支持Promise或者强制开启宏任务,那么,会按照如下顺序发起宏任务:

  1. 优先检测是否支持原生 setImmediate(这是一个高版本 IE 和 Edge 才支持的特性)
  2. 如果不支持,再去检测是否支持原生的MessageChannel
  3. 如果也不支持的话就会降级为 setTimeout。

作者:娜姐聊前端 链接:www.jianshu.com/p/bfc3e319a… 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

文章分类
前端
文章标签