JS异步进阶之1:event loop、宏任务、微任务

444 阅读4分钟

一、event loop(事件循环/事件轮询)机制

  • js是单线程运行的

  • 异步要基于回调来实现

  • event loop 就是异步回调的实现原理

1.1、 js如何执行

  • 从前往后,一行一行执行
  • 如果某一行执行报错,则停止下面代码的执行
  • 先把同步代码执行完,再执行异步
console.log('start')

setTimeout(function cb1(){
    console.log('cb') // cb 即 callback
}, 500)

console.log('end')

执行结果 image.png

1.2 event loop 的执行过程

image.png

1、同步代码,一行一行放在CallStack执行

2、遇到异步,会先“记录”下,等待时机(定时、网络请求等)

3、时机到了,就移动到Callback Queue

4、如Call Stack为空(即同步代码执行完)Event Loop开始工作

5、轮询查找Callback Queue,如有则移动到Call Stack执行

6、然后继续轮询查找(永动机一样)

1.3 代码执行过程:

1. 将 console.log("Hi") 推入调用栈,调用栈会执行代码

2. 执行代码,控制台打印“Hi”,调用栈清空

3. 执行 setTimeout,setTimeout由浏览器定义,不是ES6的内容;

将定时器放到Web APIs中,到时间后将回调函数放到回调函数队列中

4. 执行完了setTimeout, 清空调用栈

5. console.log("Bye")进入调用栈,执行,调用栈清空

6. 同步代码被执行完,,回调栈空,浏览器内核启动时间循环机制

7. 五秒之后,定时器将cb1推到回调函数队列中

8. 事件循环将cb1放入调用栈

二、DOM事件和event loop的关系

dom事件也使用回调,基于 event loop 但是 dom事件不是异步的,但是它们都是基于异步的

  • js 是单线程的
  • 异步(setTimeout, ajax等)使用回调,基于 event loop
  • dom事件也使用回调,基于 event loop 但是 dom事件不是异步的,但是它们都是基于异步的

三、 什么是宏任务macroTask和微任务microTask

3.1 代码示例

console.log(100)

setTimeout( () => {
    console.log(200)
})

Promise.resolve().then(()=>{
     console.log(300)
})

console.log(400)

代码执行 顺序打印 100 400 300 200

js中执行顺序:

宏任务——>微任务——>DOM页面重渲染——>宏任务(循环)

宏任务主要有:

setTimeoutsetIntervalAjaxDOM事件

微任务:

Promiseasync/await

微任务执行比宏任务要早

微任务执行比宏任务要早(其实不太对,因为script标签执行解析也属于宏任务)

四、event-loop和DOM渲染的关系

js是单线程,且和DOM渲染共用同一个线程

因此js执行时,需要留一些时机给DOM渲染。

如下面代码所示,分析下DOM渲染的时机

const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
            .append($p1)
            .append($p2)
            .append($p3)

console.log('length',  $('#container').children().length )

如下图所示, 其实有DOM 渲染的流程 image.png

  • 每次Call Stack清空(即每次轮询结束),即同步任务执行完
  • 都是DOM重新渲染的机会,DOM结构如有改变则重新渲染
  • 然后再去触发下一次 Event Loop

五、 为什么微任务比宏任务执行更早?

const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
            .append($p1)
            .append($p2)
            .append($p3)

console.log('length',  $('#container').children().length )
alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
// (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
// 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预

代码执行效果

image.png

上图可以看到,alert() 执行,但是DOM 还没有渲染

我们点击alert 弹窗的确认,DOM渲染 如下图所示

image.png

微任务和宏任务的区别:

  • 宏任务:DOM渲染后触发,如setTimeout
  • 微任务:DOM渲染前触发,如Promise

所以微任务执行时机比宏任务要早

// 修改 DOM
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
    .append($p1)
    .append($p2)
    .append($p3)

// // 微任务:渲染之前执行(DOM 结构已更新)
// Promise.resolve().then(() => {
//     const length = $('#container').children().length
//     alert(`micro task ${length}`)
// })

// 宏任务:渲染之后执行(DOM 结构已更新)
setTimeout(() => {
    const length = $('#container').children().length
    alert(`macro task ${length}`)
})

六、微任务和宏任务的根本区别

再深入思考一下:为何两者会有以上区别,一个在渲染前,一个在渲染后?

微任务和宏任务在event loop会放在不同的地方等待

  • 微任务是ES6语法规定的(promise、async/await)

  • 宏任务是由浏览器规定的(setTimeout/setInterval、Ajax、DOM)

  • 微任务:ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何关于,即可一次性处理完,更快更及时。
  • 宏任务:ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。