搞懂JS的事件循环(Event Loop)和宏任务/微任务

更新日期: 2021-04-30阅读: 1.5k标签: 任务

事件循环与消息队列

首先大家都知道JS是一门单线程的语言,所有的任务都是在一个线程上完成的。而我们知道,有一些像I/O,网络请求等等的操作可能会特别耗时,如果程序使用"同步模式"等到任务返回再继续执行,就会使得整个任务的执行特别缓慢,运行过程大部分事件都在等待耗时操作的完成,效率特别低。

为了解决这个问题,于是就有了事件循环(Event Loop)这样的概念,简单来说就是在程序本身运行的主线程会形成一个"执行栈",除此之外,设立一个"任务队列",每当有异步任务完成之后,就会在"任务队列"中放置一个事件,当"执行栈"所有的任务都完成之后,会去"任务队列"中看有没有事件,有的话就放到"执行栈"中执行。

这个过程会不断重复,这种机制就被称为事件循环(Event Loop)机制。


宏任务/微任务

宏任务可以被理解为每次"执行栈"中所执行的代码,而浏览器会在每次宏任务执行结束后,在下一个宏任务执行开始前,对页面进行渲染,而宏任务包括:

  • script(整体代码)
  • setTimeout
  • setInterval
  • I/O
  • UI交互事件
  • postMessage
  • MessageChannel
  • setImmediate
  • UI rendering

微任务,可以理解是在当前"执行栈"中的任务执行结束后立即执行的任务。而且早于页面渲染和取任务队列中的任务。宏任务包括:

  • Promise.then
  • Object.observe
  • MutaionObserver
  • process.nextTick

他们的运行机制是这样的:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

在了解了宏任务和微任务之后,整个Event Loop的流程图就可以用下面的流程图来概括:



例子

如无特殊说明,我们用setTimeout来模拟异步任务,用Promise来模拟微任务。

主线程上有宏任务和微任务

console.log('task start');

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

new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('Promise.then')
})

console.log('task end');

//----------------------执行结果----------------------
// task start
// new Promise
// task end
// Promise.then
// setTimeout

这个例子比较简单,就是在主任务上加了一个宏任务(setTimeout),加了一个微任务(Promise.then),看执行的顺序,打印出了主任务的task start、new Promise、task end,主任务完成,接下来执行了微任务的Promise.then,到此第一轮事件循环结束,去任务队列里取出了setTimeout并执行。

在微任务中添加宏任务和微任务

跟上个例子相比,我们在Promise.then里加上一个setTimeout和一个Promise.then。

console.log('task start');

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

new Promise((resolve, reject)=>{
    console.log('new Promise1')
    resolve()
}).then(()=>{
    console.log('Promise.then1')
    setTimeout(()=>{
        console.log('setTimeout2')
    },0)
    new Promise((resolve, reject)=>{
       console.log('new Promise2')
        resolve()
    }).then(()=>{
        console.log('Promise.then2')
    })
})

console.log('task end');

//----------------------执行结果----------------------
// task start
// new Promise1
// task end
// Promise.then1
// new Promise2
// Promise.then2
// setTimeout1
// setTimeout2

猜对了么,正常的主任务没有变化,只是在执行第一次微任务的时候,发现了一个宏任务,于是被加进了任务对了。遇到了一个微任务,放到了微任务队列,执行完之后又扫了一遍微任务队列,发现有微任务,于是接着执行完微任务,到这,第一遍事件循环才结束,从任务队列里拿出了两次setTimeout执行了。

在异步宏任务中添加宏任务和微任务

其他无异,把刚才添加到Promise.then中的内容添加到setTimeout中。

console.log('task start')

setTimeout(()=>{
    console.log('setTimeout1')
    setTimeout(()=>{
        console.log('setTimeout2')
    },0)
    new Promise((resolve, reject)=>{
       console.log('new Promise2')
        resolve()
    }).then(()=>{
        console.log('Promise.then2')
    })
},0)

new Promise((resolve, reject)=>{
    console.log('new Promise1')
    resolve()
}).then(()=>{
    console.log('Promise.then1')
})

console.log('task end')

//----------------------执行结果----------------------
// task start
// new Promise1
// task end
// Promise.then1
// setTimeout1
// new Promise2
// Promise.then2
// setTimeout2

第一遍主任务执行大家都很明白了,到Promise.then1结束,然后取任务队列中的setTimeout,执行过程中又发现了一个setTimeout,放到任务队列中,并且发现一个Promise.then2,把这个微任务执行完之后,第二遍事件循环才结束,然后开始第三遍,打印出了setTimeout2。

加入事件冒泡

事件循环遇到事件冒泡会发生什么?

<div class="outer">
  <div class="inner"></div>
</div>
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

function onClick() {
  console.log('click');
  
  setTimeout(function() {
    console.log('setTimeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('new Promise');
  });
}

inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

点击inner,结果:

click		//inner的click
promise		//inner的promise
click		//outer的click
promise		//outer的promise
timeout		//inner的timeout
timeout		//outer的timeout

我觉得解释应该是这样的:
1、开始执行,因为事件冒泡的缘故,事件触发线程会将向上派发事件的任务放入任务队列。接着执行,打印了click,把timeout放入任务队列,把promise放入了微任务队列。
2、执行栈清空,check微任务队列,发现微任务,打印promise,第一遍事件循环结束。
3、从任务队列里取出任务,执行outer的click事件,打印click,把outer的timeout放入任务队列,把outer的promise放入了微任务队列。执行inner放入任务队列的timeout。
4、执行栈清空,check微任务队列,发现微任务,打印promise,第二遍事件循环结束。
5、从任务队列里取出任务,把timeout打印出来。

JS触发上面的click事件

一样的代码,只不过用JS触发结果就会不一样。
对代码做了稍稍改变,将click拆分成两个方法,方便追踪是谁被触发了。

var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

const onInnerClick = (e) => {
  console.log('inner cilcked');

  setTimeout(function() {
    console.log('inner timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('inner promise');
  });
}

const onOuterClick = (e) => {
  console.log('outer clicked');

  setTimeout(function() {
    console.log('outer timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('outer promise');
  });
}

inner.addEventListener('click', onInnerClick);
outer.addEventListener('click', onOuterClick);

inner.click();

执行结果:

inner cilcked
outer clicked
inner promise
outer promise
inner timeout
outer timeout

之所以会出现这样的差异,我的理解是JS代码执行中的click事件,分发了一个同步的冒泡事件。所以在第一个click事件结束之后,调用栈中有outer的click事件,所以出现了两个连续的click。

这也是根据结果猜测过程,心里没底。

参考资料:
什么是 Event Loop?
Tasks, microtasks, queues and schedules
js中的宏任务与微任务

原文链接:https://www.cnblogs.com/EaVango/archive/2021/04/30/14722428.html

链接: https://www.fly63.com/article/detial/10256

PHP实现执行定时任务的几种思路详解

PHP定时任务是一个非常有意思的东西,虽然说实话,用系统的php.exe去直接执行php文件的效率更高,但是对于很多普通站长而言,虚拟主机是无法做到直接php执行原生程序的。本文仅提供一些解决的思路

使用 queueMicrotask 来执行微任务

写这篇文章的原因是因为,这几天在看 core-js 的源码,然后发现了 queueMicrotask 的实现。由于之前做的项目,对于微任务的执行需求,一般是使用 asap 这个库来完成的,如果没有使用这个库的话

js中特殊的宏任务

目前只有IE10+和NodeJS支持该API。立即触发回调函数,使其进入宏任务队列(macro task queue),比setTimout(fn, 0)的执行顺序要快,性能也更高。因为setTimeout(fn,0)实质上会有4ms的延迟。

Node.js实现定时任务

在本文中,我们将研究如何在 Node 程序中创建和使用 Cron 作业。为此我们将创建一个简单的程序,该应用程序会自动从服务器中删除自动生成的 error.log 文件。 Cron 作业的另一个优点是

JS微任务 宏任务,Promise、setTimeout、setImmediate运行顺序实测

虽然理论上应当先运行Promise,再运行setTimeout。但是由于历史版本或使用polyfill,使得Promise未必优先运行。setImmediate未必比setTimeout早运行

关于宏任务、微任务和事件循环

为什么会是这样的输出顺序呢?这就要提到事件循环、宏任务和微任务的概念了。众所周知,JavaScript是一个单线程的语言,单线程意味着代码会自上而下依次执行,如果有一个耗时的操作,那么页面就会卡死,基于此,便有了异步的概念

如何在 JavaScript 中使用宏

在语言当中,宏常见用途有实现 DSL 。通过宏,开发者可以自定义一些语言的格式,比如实现 JSX 语法。在 WASM 已经实现的今天,用其他语言来写网页其实并不是没有可能

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!