Event Loop、宏任务、微任务到底什么关系?

本文详细介绍了CPU、进程、线程以及JavaScript中的Event Loop机制,包括宏任务和微任务的执行顺序。JavaScript是单线程的,但通过Event Loop协调同步和异步任务的执行。宏任务包括script、setTimeout等,它们按顺序执行。微任务如Promise.then、Async/Await则在当前宏任务执行完后立即执行。文章还探讨了定时器的不准确性、渲染过程以及async/await的执行细节,并给出了相关问题的解答和参考资料。

CPU、进程、线程

CPU、进程、线程之间的关系

cpu:计算机的核心,承担了所有的计算任务;

进程: 进程就好比工厂的车间,它代表CPU所能处理的单个任务,CPU使用时间片轮转进度算法来实现同时运行多个进程;

线程: 线程就好比车间里的工人,一个进程可以包括多个线程,多个线程共享进程资源;

浏览器是多进程的,其中包含了主进程、第三方插件进程、GPU进程、渲染进程,其中有渲染进程,也就是我们说的浏览器内核;

浏览器内核又包含多条线程

GUI渲染线程:

  • 负责渲染页面,布局和绘制
  • 页面需要重绘和回流时,该线程就会执行
  • 与js引擎线程互斥,防止渲染结果不可预期

JS引擎线程:

  • 负责处理解析和执行javascript脚本程序
  • 只有一个JS引擎线程(单线程)
  • 与GUI渲染线程互斥,防止渲染结果不可预期

事件触发线程:

  • 用来控制事件循环(鼠标点击、setTimeout、ajax等)
  • 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中

定时触发器线程:

  • setInterval与setTimeout所在的线程
  • 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的
  • 计时完毕后,通知事件触发线程

异步http请求线程:

  • 浏览器有一个单独的线程用于处理AJAX请求
  • 当请求完成时,若有回调函数,通知事件触发线程

同步与异步

javascript是单线程的,但是浏览器并不是单线程的;例如我们使用了setTimeout,在浏览器中就会有一个定时触发器线程去为我们执行代码;也就是说这些内部的API会用单独的线程去执行;

同步还是异步指的是 运行环境提供的API是以同步或异步模式的方式去工作;

js中的任务分为同步任务和异步任务

同步模式:

同步任务都在JS引擎线程上执行,形成一个执行栈

代码当中的任务依次执行,执行顺序与代码的编写顺序完全一致,在单线程的情况下,大多数任务都会以同步模式的方式去执行;但是如果其中有个任务执行的时间过长,就会产生阻塞,意味着页面会卡顿或者无法操作,所以需要有异步模式,来解决程序中无法避免的耗时操作;

console.log(1)

function b() {
    console.log(3)
}

function a() {
    console.log(2)
    b()
}
a()
console.log(4)

异步模式:

事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到任务队列中;

不会去等待这个任务的结束才开始下一个任务,开启过后就立即往后执行下一个任务,后续逻辑一般会通过回调函数的方式来定义;

console.log(1)
setTimeout(function time1() {
    console.log(4)
}, 1800)
setTimeout(function time2() {
    console.log(3)
    setTimeout(function inner () {
        console.log(5)
    }, 1000)
}, 1000)
console.log(2)

Event Loop

执行栈中所有同步任务执行完毕,此时JS引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行;

  • JS引擎线程只执行执行栈中的事件
  • 执行栈中的代码执行完毕,就会读取事件队列中的事件
  • 事件队列中的回调事件,是由各自线程插入到事件队列中的
  • 如此循环

img

在js中,任务又可以被细分为宏任务和微任务;

宏任务

我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他。

前面我们提到过JS引擎线程GUI渲染线程是互斥的关系,浏览器为了能够使宏任务DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。

常见的宏任务:

  • script(可以理解为外层同步代码)
  • setTImeout
  • setInterval

一个例子:

document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';

我们在控制台输入上面的代码会看到的结果是,页面背景会在瞬间变成灰色,以上代码属于同一次宏任务,所以全部执行完才触发页面渲染,渲染时GUI线程会将所有UI改动优化合并,所以视觉效果上,只会看到页面变成灰色。

第二个例子:

document.body.style = 'background:blue';
setTimeout(function(){
    document.body.style = 'background:black'
},0)

我们继续在控制台输入上面的代码会看到,页面先显示成蓝色背景,然后瞬间变成了黑色背景,这是因为以上代码属于两次宏任务,第一次宏任务执行的代码是将背景变成蓝色,然后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色。

微任务

我们已经知道宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成在当前宏任务执行后立即执行的任务。也就是说,当宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完。

常见的微任务:

  • Promise.then
  • Async/Await
  • process.nextTick

第一个例子:

document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
    console.log(2);
    document.body.style = 'background:black'
});
console.log(3);

控制台输出 1 3 2 , 是因为 promise 对象的 then 方法的回调函数是异步执行,所以 2 最后输出

页面的背景色直接变成黑色,没有经过蓝色的阶段,是因为,我们在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务, 在微任务中将背景变成了黑色,然后才执行的渲染

第二个例子:

setTimeout(() => {
    console.log(1)
    Promise.resolve(3).then(data => console.log(data))
}, 0)

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

//  1 3 2

上面代码共包含两个 setTimeout ,也就是说除主代码块外,共有两个宏任务, 其中第一个宏任务执行中,输出 1 ,并且创建了微任务队列,所以在下一个宏任务队列执行前, 先执行微任务,在微任务执行中,输出 3 ,微任务执行后,执行下一次宏任务,执行中输出 2。

总结

  • 执行一个宏任务(栈中没有就从事件队列中获取)

  • 执行过程中如果产生了新的微任务,就将它添加到微任务的任务队列中

  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)

  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染

  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

img

一些问题:

Q:定时器的时间一定准确嘛?

A:不一定

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

new Promise((resolve, reject) => {
    resolve()
}).then(() => {
    for (var i = 0; i < 3000000000; i++) { }
    console.log(2222222)
})

循环分析:
第一轮循环:
执行setTimeout,定时器任务线程接手;
执行promise中的resolve(),在当前微任务队列中放入fn2;
当前宏任务队列没有任务,执行微任务队列fn2;
执行完成,GUI渲染引擎渲染,进入下一轮循环;
第二轮循环:
定时器任务线程触发条件达成,将fn1放入宏任务队列;
此时执行栈为空,事件触发线程发现宏任务队列有任务fn1,放入执行栈执行;
fn1执行完成,事件触发线程再次循环发现执行栈和任务队列为空;
GUI渲染引擎接手,渲染,进入下一轮循环;

Q:既然渲染阶段是在这个宏任务的执行栈清空,微任务队列清空之后由GUI渲染线程接管进行。那为什么我在循环appendChild之后,立马用document.getElements能获取插入的节点呢?这个时候还没有渲染是怎么获取到的?

A:appendchild只是修改了dom树,但是浏览器并没有渲染,要等到下一次gui渲染进程渲染之后界面上才会出现,但是在js中你已经可以访问这个dom了;

一些细节:

async/await执行顺序

我们知道async隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。我们来看个例子:

console.log('script start')

async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()

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

new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})

console.log('script end')
 // 旧版输出如下,新版有改动
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

新版的chrome浏览器中不是如上打印的,因为chrome优化了,await变得更快了,输出为:

// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout

但是这种做法其实是违法了规范的,当然规范也是可以更改的,这是 V8 团队的一个 PR ,目前新版打印已经修改。 知乎上也有相关讨论,可以看看 www.zhihu.com/question/26…

如果await后面跟的是一个异步函数的调用:

console.log('script start')

async function async1() {
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2 end')
    return Promise.resolve().then(()=>{
        console.log('async2 end1')
    })
}
async1()

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

new Promise(resolve => {
    console.log('Promise')
    resolve()
})
.then(function() {
    console.log('promise1')
})
.then(function() {
    console.log('promise2')
})

console.log('script end')

输出为:

// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout

参考资料:1. 「前端进阶」从多线程到Event Loop全面梳理
2. 面试题:说说事件循环机制(满分答案来了)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Whoopsina

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值