乘风破浪 激流勇进
你好!欢迎来看Tuziki's Planet !

Javascript中的宏任务与微任务,与事件循环,与在node中的区别

宏任务与微任务

javascript中所有可以被执行的代码句,都可以是一个任务


任务又分为同步任务(synchronous) 和 异步任务(asynchronous);


同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;


异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行;


异步任务之中又分为两种任务:宏任务微任务


宏任务macrotask:是指那些由浏览器或Node.js环境发起的任务,setTimeOut(),setInterVal();requestAnimationFrame (浏览器独有);I/O线程;setImmediate (Node独有)等。

执行优先级:主代码块 > setImmediate > MessageChannel > setTimeout / setInterval (大部分浏览器会把DOM事件回调优先处理,因为要提升用户体验给用户反馈,其次是network IO操作的回调,再然后是UI render,之后的顺序会因不同浏览器会有所不同,这里不做过多讨论)


微任务microtask:是指那些由JavaScript引擎发起的任务promise.then()方法,Object.observe,MutationObserver,async/await等。

执行优先级:process.nextTick > Promise = MutationObserver


JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事;因此,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

当浏览器扫描到上例的异步任务后,不会立马执行,浏览器会将这些任务放入js相对应的任务队列中。扫描到宏任务放入宏任务的任务队列,扫描到微任务放入微任务的任务队列。然后继续向下扫描并立马执行掉同步任务。

当所有的同步任务被执行完毕以后。浏览器才开始执行异步任务队列。

在异步任务队列中。浏览器会优先执行微任务队列里面的所有任务,当微任务队列中的所有任务被全部执行完毕后。才会去执行宏任务队列中的一个宏任务。

注意:执行完所有微任务后只会去宏任务队列中执行一个宏任务(不论宏任务队列中压了多少个宏任务,只执行一个),然后就会重新扫描微任务队列,查看是否有可执行的微任务。如果有,再次执行完所有的微任务,再回到宏任务队列中。


简单来说,宏任务和微任务的执行顺序是这样的:

  • 事件循环从宏任务队列中取出最早的一个任务,执行它。
  • 执行完毕后,检查微任务队列,如果有微任务,就依次执行所有的微任务,直到清空微任务队列。
  • 如果微任务队列为空,或者执行完所有的微任务,就进行渲染更新(如果有必要)。
  • 然后再从宏任务队列中取出下一个任务,重复上面的步骤。
  • 也就是说,每个宏任务之后,都会执行所有的微任务,然后再执行下一个宏任务。这样可以保证微任务能够尽快地得到执行,而不会被其他宏任务阻塞。
  • 流程图如下


  console.log('script start'); // 宏任务1
  
  setTimeout(function() {
    console.log('setTimeout'); // 宏任务2
  }, 0);
  
  Promise.resolve().then(function() {
    console.log('promise1'); // 微任务1
  }).then(function() {
    console.log('promise2'); // 微任务2
  });
  
  console.log('script end'); // 宏任务1

输出结果是:

  script start
  script end
  promise1
  promise2
  setTimeout

解释如下:

首先执行宏任务1(整个脚本),打印出script start和script end。

然后检查微任务队列,发现有两个微任务(由Promise产生),依次执行它们,打印出promise1和promise2。

然后进行渲染更新(如果有必要)。

然后从宏任务队列中取出下一个宏任务(由setTimeout产生),执行它,打印出setTimeout。


事件循环(event loop)

上面说了宏任务和微任务,是事件循环(event loop)讲解的储备。

浏览器扫描到同步任务后会直接将同步任务压入主线程的执行栈,在扫描到异步的任务的时候会加入宏任务/微任务队列。

在执行完所有同步任务后,浏览器会去微任务队列查询出所有的微任务,并压入执行栈,执行完所有微任务后浏览器会去宏任务队列中查询一个宏任务并压入执行栈。

执行完这个宏任务后浏览器又会去微任务队列中找出所有微任务执行…

如此一直循环。

这个过程,我们称为事件循环(event loop)


事件循环在javascript和Nodejs中的区别:

JavaScript的事件循环和Node.js的事件循环有一些区别,主要是在宏任务队列和微任务队列的划分上。

JavaScript的事件循环是由浏览器或V8引擎实现的,它只有一个宏任务队列,用来存放setTimeout、setInterval、requestAnimationFrame等宏任务,以及一个微任务队列,用来存放Promise.then、MutationObserver、queueMicrotask等微任务。每个宏任务执行完毕后,都会清空微任务队列,然后再执行下一个宏任务。

Node.js的事件循环是由libuv库实现的,它有多个宏任务队列,分别用来存放不同类型的宏任务,比如timers、pending callbacks、poll、check、close callbacks等。每个宏任务队列执行完毕后,都会清空微任务队列,然后再执行下一个宏任务队列。34

这样的区别导致了JavaScript和Node.js在处理异步任务时可能有不同的执行顺序。比如,在JavaScript中,如果同时使用了setTimeout和setImmediate,那么通常会先执行setTimeout的回调,再执行setImmediate的回调。但是,在Node.js中,如果同时使用了这两个函数,并且在主模块中调用(不在任何I/O回调中),那么通常会先执行setImmediate的回调,再执行setTimeout的回调。5

这是因为在JavaScript中,只有一个宏任务队列,所以先进入队列的宏任务先执行;而在Node.js中,有多个宏任务队列,而且每个队列都有自己的优先级和执行时机。具体来说,在Node.js中,如果在主模块中调用了这两个函数,那么它们会被分别放入不同的宏任务队列:setTimeout会被放入timers队列,而setImmediate会被放入check队列。而在事件循环中,如果没有其他I/O操作或定时器到期,那么事件循环会先进入到check阶段,执行所有的check回调(包括setImmediate),然后再进入到下一个循环周期的timers阶段,执行所有的timers回调(包括setTimeout)。所以,在这种情况下,setImmediate会比setTimeout先执行。

当然,这种情况并不一定总是成立,因为Node.js中的事件循环还受到其他因素的影响,比如I/O操作、定时器到期等。所以,在Node.js中使用异步任务时要注意区分不同类型的宏任务和微任务,并且不要依赖于特定的执行顺序。


标签:Javascript
返回列表
返回顶部←