宏任务微任务是什么?一文读懂JS工作流程和事件循环

我们知道JavaScript是单线程,那它的工作流程到底是怎样的呢?

JS引擎

我们用的最多的就是谷歌的V8引擎。

内存堆

当我们写let a = '123'时,引擎会在内存分配一个位置存放这个变量,这个区域叫内存堆Memory Heap。

调用栈

当我们调用function printA () { console.log(a) }时,引擎会把这个要执行的代码放入栈中,这个地方叫调用栈Call Stack。

调用栈中最后入栈的代码会最先执行,执行完后会被移除。

因此这段代码的执行顺序是:printA入栈 -> console.log入栈 -> 执行console.log -> console.log出栈 -> printA出栈,这段代码就执行完成了。

当我们写了个死循环时,调用栈中函数的调用次数超过了调用栈的大小,我们会看到这样的错误:

Web API

其中console是浏览器提供的Web API,还有比如DOM,AJAX,setTimeout都属于Web API。

异步

如果调用栈中的函数需要长时间执行,那么浏览器会被阻塞,于是引入了异步的概念,异步函数中有一个回调函数,意思是,当异步函数执行和其他阻塞结束后再执行回调函数。AJAX和setTimeout就是这样的。

事件循环

有了异步函数,事件循环就是用来监控调用栈和回调队列,调度执行回调函数的。

回调队列Callback Queue

比如我在某个元素上绑定了onClick和onMouseEnter的回调函数:

js 复制代码
function onMouseEnter() {}
function onClick() {}

document.addEventListener('click', onMouseEnter);
document.addEventListener('click', onClick);

在浏览器中我的鼠标进入了这个元素的区域并点击了这个元素 ,那么Web API会把这两个回调函数按鼠标的行为顺序放入回调队列中.

当调用栈为空时,事件循环会从回调队列中取第一个进入调用栈准备执行。事件循环遍历一遍回调队列称为一个tick。

当我们写setTimeout(myCallback, 5000),其实意味着5秒后Web API会把myCallback放入回调队列,而只有当调用栈为空时,myCallback才会被事件循环放入调用栈执行,因此如果调用栈有阻塞或回调队列中还有微任务Micro Task时,myCallback都会等待更长时间才会被执行。

任务队列Task Queue

ES6引入了任务队列的概念,也就是升级版的回调队列。任务队列中有宏任务队列MacroTask Queue和微任务队列MicroTask Queue,事件循环会先执行当前宏任务队列,执行完后检查宏任务当中是否生成有微任务队列,执行完微任务队列后后再取下一个宏任务执行。

宏任务Macro Task

宏任务是颗粒度较大的任务,包括:

  • 所有同步任务
  • I/O操作,如文件读写、数据库数据读写等
  • setTimeout、setInterval
  • setImmediate(Node.js环境)
  • requestAnimationFrame
  • 事件监听回调函数等
  • ...

微任务Micro Task

微任务是颗粒度较小,优先级较高的任务,包括:

  • Promise的then、catch、finally
  • async/await中的代码
  • Generator函数
  • MutationObserver
  • process.nextTick(Node.js 环境)
  • ...

举例

下面我们在某个元素上绑定onClick和onMouseUp,并在这两个回调函数中都加入异步函数:

js 复制代码
function onMouseUp() {
    setTimeout(() => { 
        console.log(1); 
    }, 0);
    console.log(2)
}
function onClick () {
    console.log(3);
    new Promise(resolve => {
        console.log(4);
    }).then(() => {
        console.log(5);
    });
}

要想知道console的顺序,我们先在代码中做出标记,参考注释:

js 复制代码
function onMouseUp() {       // <=== 回调函数,宏任务
    setTimeout(() => {       // <=== 回调函数中的异步函数,宏任务
        console.log(1);      // <=== 回调函数中的同步函数,宏任务
    }, 0);
    console.log(2)           // <=== 回调函数中的同步函数,宏任务
}

function onClick () {        // <=== 回调函数,宏任务
    console.log(3);          // <=== 回调函数中的同步函数,宏任务
    new Promise(resolve => { // <=== 回调函数中的同步步函数,宏任务(新建promise是宏任务)
        console.log(4);      // <=== 回调函数中的同步函数,宏任务
        resolve();
        console.log(5);
    }).then(() => {          // <=== promise中的then是微任务
        console.log(6);      // <=== 回调函数中的同步函数,宏任务
    });
    console.log(7);
}
  1. 首先浏览器上我们点击那个元素,dom会先处理mouseUp行为再处理click行为:
  1. 由此我们可以得出一个任务队列:
  1. Web API会根据setTimeout创建一个timer:
  1. 任务队列中的任务会被依次入调用栈-执行-出调用栈:

此时console台有:2,3.

  1. new Promise中的回调是立即执行的,是同步函数,而then中的回调是异步函数,是微任务,此时任务队列如下:

此时console台有:2,3.

  1. 任务队列的任务依次执行后,timer中的任务将最后执行:

此时console台有:2,3,4,5,7,6.

  1. 随着最后一个任务执行,console出的数字依次为:2,3,4,5,7,6,1。

参考

相关推荐
小码哥_常2 小时前
一套自己部署的 IM,客户端是同一份 Flutter
前端·后端
蜗牛攻城狮5 小时前
微前端四大方案选型对比与最佳实践:qiankun、micro-app、wujie、Module Federation
前端·微前端·qiankun·前端架构·micro-app·wujie
她说可以呀6 小时前
Vue插槽
前端·javascript·vue.js
qq_448011166 小时前
Vue + Jinja2两套模版与法冲突造成undefinedError
前端·javascript·vue.js
aixingpan7 小时前
aixingpan.cn API开发文档:前端查询夏令时和时区接口指南
前端·php
Yuzhiyuxia7 小时前
【AI Agent面试系列】AI Agent记忆机制知识点1
人工智能·面试·职场和发展
2601_958352907 小时前
23.5×19mm 换掉一整条音频链?|A-68 集成了什么:ADC / DAC / 24.576MHz 时钟 / AEC / 波束降噪,主控仅需 6 根线
前端·音视频·ai降噪·回声消除·语音模块
志尊宝8 小时前
AI原生前端开发(039):AI辅助的Nuxt 4全栈开发
前端·vue.js·ai-native
FRing039 小时前
JavaScript入门到精通三十二(深拷贝、浅拷贝、递归函数)
开发语言·前端·javascript
excel9 小时前
双 Token 与前端使用 Crypto 公私钥鉴权换 Token,哪种方案更好?
前端·后端