一篇弄懂宏任务、微任务和事件循环 Event Loop🧨

249 阅读5分钟

哈喽,我是前端菜鸟JL😄 下面分享一下事件循环这个专题

Javascript语言

特点单线程,非阻塞,动态语言

单线程:一个主线程处理所有任务

为什么?

被设计成单线程主要是为了操作DOM,假如多线程,有可能同时操作同一个DOM节点造成操作冲突

非阻塞:可以理解为异步执行任务时候,这个任务无法立刻返回,会被主线程挂起,然后等待回调

web worker技术:让JavaScript成为一门多线程语言

存在限制

  • 所有新线程是主线程的子线程,受主线程完全控制,不能独立执行
  • 子线程没有执行I/O能力,只能进行计算等

js任务分为同步任务、异步任务

同步任务:页面骨架和元素渲染

异步任务:加载图片、视频之类占用资源耗时较多的任务

内核:决定渲染语言的方式,体现浏览器渲染结果的差异性,webkit依旧是霸主,Blink

渲染过程

HTML解释器(DOM树)->CSS解释器(样式规则CSSOM树)->图层布局计算模块->视图绘制模块->js渲染

image.png

eventloop 事件循环(非堵塞特性) 看上图

  1. 同步任务进入主线程,异步任务进入Event Table并注册函数
  2. Event Table任务执行完后,将对应回调函数移入Event Queue(事件队列)中
  3. 主线程执行栈任务执行完后,会读取Event Queue对应函数,进入主线程执行
  4. 上述过程不断重复,就是事件循环

概括

  • js是单线程语言,而另一大特性是非阻塞;
  • js引擎在遇到异步事件时候不会一直等待返回结果,而是将其挂起,执行主线程的同步任务;
  • 当异步任务返回结果后,会将这个事件加入Event Queue(事件队列)中,事件队列任务不会立即执行,只有当js引擎的monitring process(进程监控)监控到主线程执行栈为空时候,才会调用事件队列里面的事件进行执行,然后执行其中的同步任务,不断循环,即事件循环。

image.png

怎么知道主线程执行栈为空?

JS引擎存在monitoring process进程(进程监控),会不断检查主线程执行栈是否为空,为空后就读取Event Queue事件队列函数进行执行。

当Javascript代码执行时候会将不同的变量存在内存的不同位置:

堆:一些对象

栈:基础类型,对象的指针

执行栈:

当我们调用一个方法,js会生成与这个方法对应的执行环境,即执行上下文,包含这个方法的私有域,this指向,变量等;

由于js是单线程的,所以当调用一系列方法时候,同一时间执行一个任务,将这些执行存在一个地方,就叫做执行栈。

当执行第一个方法时候,执行栈会进入这个方法对应的执行环境,执行完这个方法代码后,就会退出执行环境并销毁

在这个执行环境中还可以调用其他方法,甚至是自己,代价无非就是在执行栈中多加一个执行环境

异步任务

执行优先级不相同,又分为微任务(micro task)、宏任务(macro task)

宏任务:

  • setInterval()
  • setTimeout()

微任务:

  • promise.then()
  • Async/Await(实际上就是promise)
  • queueMicrotask()
  • new MutationObserver()

当执行栈为空,事件队列中的宏任务和微任务会被分配到对应的宏任务队列和微任务队列中,若存在微任务,直至微任务执行完毕后再执行宏任务

微任务永远在宏任务之前执行

另外ES6引入作业队列的概念

Promise使用了该队列,会尽快执行该队列异步函数

即Promise可以看成同步任务,而Promise.then则看成微任务

常用:setTimeout(()=>{}, 0),nextTick()

注意

setTimeout(()=>{},0)相当于宏任务,而nextTick()则会在当前tick执行结束后,下一个tick开始前被优先调用,所以nextTick()比setTimeout()快的多

当要确保在下一个事件循环迭代中代码已被执行,则使用nextTick()

image.png

执行顺序为:

同步执行的代码

console.log=promise>resolve>promise.nextTick>promise.then>setTimeout>setImmediate

直接来道题:

setTimeout(function() {
    console.log(1)
})
new Promise(function(resolve, reject) {
    console.log(2)
    resolve(3)
}).then(function(val) {
    console.log(val)
})

结果为:
2
3
1

解析:可以看出,setTimeout为宏任务,先被挂起,promise作业队列相当于同步任务,先输出2,接着.then属于微任务,当执行栈为空时候,先执行微任务,输出3,随后是宏任务1

再来看看这道题
(function test(){
    setTimeout(function() {
        console.log(4)
    }, 0)
    new Promise((resolve) => {
        console.log(1)
        for(let i=0; i<10000; i++) {
            i === 9999 && resolve()
        }
        console.log(2)
    }).then(function() {
        console.log(5)
    })
    console.log(3)
})()

输出结果为:
1
2
3
5
4

分析:可以看到.then是微任务,setTimeout是宏任务,其他可以看做同步任务。

必须是当前执行栈为空的时候,主线程才会 查看微任务队列是否有事件存在。

宏任务和微任务区别区别

微任务MicroTask

在一次事件循环Tick中,向事件队列插入多个微任务,则新加入微任务会早于下一次循环执行

原因:因为事件循环会持续调用微任务直至微任务队列为空,不会留存

宏任务MacroTask

区别于微任务,宏任务在一次事件循环Tick中插入则会在下一次循环中执行。

node中的事件循环

node基于chromo v8引擎作为js解释器,事件循环则基于libuv引擎驱动对应node api实现

node 中也有宏任务和微任务,与浏览器中的事件循环类似:

宏任务(macro-task) 包括

setTimeout()

setInterval()

setImmediate()

I/O 操作

微任务(micro-task) 包括

process.nextTick()(与普通微任务有区别,在微任务队列执行之前执行)

promise.then()等。