js事件循环机制和实例讲解(Event Loop)
前言
相信大家都听过一句话,js是单线程的语言,那么这句话如何理解呢?今天就来讨论一下js中的事件循环,理解完事件循环机制,你就会对这句话有更深刻的理解。
事件循环(event loop)
同步与异步代码
let a = 1
console.log(a);
setTimeout(function () {
a++
}, 1000)
console.log(a);
问:a打印多少? 答:1,1。因为定时器是一个耗时的代码,同步代码是不需要耗时执行的,异步代码是需要耗时执行的,JSV8引擎会先执行同步代码再执行异步代码
到这,你已经了解了什么是同步代码什么是异步代码。
同步代码
:不需要耗时执行的代码。异步代码
:需要耗时执行的代码。- JS
V8引擎
会先执行同步代码再执行异步代码。
进程与线程
大家大学阶段应该都学习过一门课程叫做《操作系统》
,这门课程对进程与线程都有深刻的讲解。
进程
:是操作系统中程序的一次执行过程,是资源分配的基本单位。它拥有独立的地址空间、内存、文件等资源。线程
:是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。线程共享所属进程的资源,但也有自己少量的私有资源,如程序计数器、栈等。- 多个线程可以在一个进程内并发执行,共同完成进程的任务,提高系统的并发性和效率。
- 例如:一个浏览器的tab页面,就会有
渲染线程、js线程、http线程
,这些线程通力合作,共同完成进程任务。 - 做一个假设,我们有一份html代码,有一个p标签hello,但是在p标签之前引入了一份js代码。这个hello想要展示出来一定是会等到js执行完了才轮到它执行,例如我们自己写了一份js代码,这份js代码中用到了引用的js代码,如果不是前面的js代码先加载完的话,那我们这一份js代码就报错了。
因此:js的加载会阻塞页面的渲染,即:在我们的例子中,渲染线程与js线程是互斥的(不能同时工作),这就从侧面说明了一个问题:js是单线程的
- 例如:一个浏览器的tab页面,就会有
js的单线程
jd的v8引擎在执行js的过程中,只有一个线程会工作。
let a = 1
console.log(a);
setTimeout(function () {
a++
}, 1000)
console.log(a);
还是刚才的例子,为什么js中会有同步代码和异步代码
,如果第二行代码打印完了之后,到了第四行,第四行如果要你等一天时间,才能执行后面的代码,就不完蛋了吗,因为js腾不出手,所以才有了同步与异步这个机制,先把异步的比如这个定时器给他挂起来,腾出手来先去执行下面的同步代码打印a,然后全部执行完了,再执行异步,这样效率就提高了许多。
不论一个语言是单线程的还是多线程的,都各有各自的优势,例如多线程的语言,毋庸置疑,执行效率高。
那么单线程语言有什么优势呢?
js单线程的优势
js打造之初就是想作为一个浏览器的脚本语言,因此如果它占用用户过多的性能,这门语言会被嫌弃。
- 节约性能:不需要过多考虑多线程环境下资源竞争、死锁等复杂情况,代码逻辑相对简单清晰,降低了开发难度和出错概率。
- 节约上下文切换的时间:例如有一个循环语句要执行1s,然后对a变量做一个操作,一个定时器也要执行1s,也对a变量做一个操作,多线程语言就会对其中一个上锁,先执行完另一个然后切换回这个解锁。这就存在上下文切换的耗时。
- 便于与浏览器交互:能更好地与浏览器的单线程模型相契合,确保页面渲染和用户交互的稳定性,不会因为多线程竞争而导致页面显示异常等问题。
微任务与宏任务
let count = 0;
function a(){
setTimeout(()=>{
count++;
},1000)
}
function b(){
console.log(count);
}
a();
b();
以上面代码为例子,还是一样,执行到13行,调用a,发现a是一个异步代码,因此先挂起了,执行b的调用,然后发现count还是0,因此打印0,然后过1s执行count++。倘若我们这个地方写的不是一个定时器隔1s执行,而是一个http请求,但是这个请求耗时是说不准的,机器性能好,速度就快,性能差就慢。因此就会存在一些场景,你认为这个代码是耗时的但是它又不耗时,你说它耗时,但是它又几乎不耗时。因此仅仅有异步代码这个概念就没办法解决所有的应用场景了,那么官方就在异步下又区分了一个微任务与宏任务。
- 微任务与宏任务,都是异步代码
- 微任务:promise.then(),proces.nextTick(),mutationObserver()
- 宏任务:script,setTimeout,setInterval,setImmediate,I/O,UI rendering
事件循环机制
- 执行同步代码(这属于是宏任务)
- 同步执行完毕后,检查是否有异步需要执行
- 如果有,则执行微任务
- 微任务执行完毕后,如果有需要就会渲染页面
- 执行异步宏任务,也是开启下一次事件循环(因为宏任务中,也一样会有同步代码、异步代码...)
面试题实战1
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve()
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(4);
})
setTimeout(function () {
console.log(5);
})
console.log(6);
// 1 2 6 3 4 5(宏里面也有周期,同步->微任务->渲染->宏)
分析过程:
- 第一行代码,执行打印1(同步代码)
- 第二行代码promise的调用,同步代码,打印2
- 第六行代码是异步代码里面的微任务,因此,加入微任务队列挂起(then1)。
- 第九行代码是异步代码里面的微任务,因此,加入微任务队列挂起(then2)。
- 第十二行代码是异步代码里的宏任务,因此,加入宏任务队列挂起(set1)。
- 第十五行代码是同步任务,打印6。
- 至此,第一次事件循环机制里的第一步,同步任务全部执行完毕,开始寻找是否有异步代码需要执行
- 如果有,执行微任务,因此then1出队列,也就是打印3,然后then2出队列,打印4。
- 微任务执行完毕后,没有发现渲染操作,因此接下来执行异步中的宏任务,也是开启了下一次事件循环,set1出队列,执行打印5.
- 最终结果:1,2,6,3,4,5。
面试题实战2
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve()
})
.then(() => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0)
})
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0)
}, 0)
console.log(7);
分析过程:
- 第一行执行,同步代码,打印1
- new了一个Promise,构造函数本身也不是异步,同步代码,执行打印2
- 调用resolve,.then就能执行了,发现是一个异步的微任务,因此入微任务队列挂起then1(打印3挂起)
- 到达十二行,发现是一个定时器,异步里面的宏任务,入宏任务队列set1
- 18行同步代码,打印7
- 至此,第一次事件循环的第一步同步代码执行结束
- 执行微任务,then1出队列,then1中有同步和异步,同步先执行,因此打印3,然后发现有个定时器,因此set2入宏任务队列。微任务执行结束
- 开始执行宏任务,set1出队列,宏任务开启一次新的事件循环,同步任务先执行,打印5,然后第二次事件循环发现了一个定时器宏任务,set3入宏任务队列,第二次事件循环的同步结束,然后去找微任务队列,发现微任务队列是空的,紧接着去宏任务队列找宏任务,开启第三次事件循环,set2出队列,因此打印4,这也意味着第二次事件循环宏任务结束,第二次事件循环结束,打印4即是第二次时间循环的结束也是第三次事件循环的开始,紧接着去微任务队列找,发现没有,然后去宏任务队列找,set3出队列,打印6。
- 因此结果:1273546
面试题3实战
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(function (resolve, reject) {
console.log('promise');
resolve()
})
.then(() => {
console.log('then1');
})
.then(() => {
console.log('then2');
})
console.log('script end');
分析过程:注意:await会将后续代码阻塞进微任务队列
- 第一行同步代码执行 打印script start
- 第10行带了了async1的调用,async2前面有await,因此async2调用,async2里面是同步,打印async2 end,然后轮到第五行代码执行,但是由于await将这行打印放到了微任务队列,因此async1 end入微任务队列
- 到达第十一行,是一个定时器,宏任务进宏任务队列set
- 达到第十四行,同步任务打印promise
- 到达第十八行,进微任务队列then1
- 达到21行,进微任务队列then2(目前微任务队列有3个,一个async1 end,then1,then2)
- 24行同步任务,打印script end
- 至此同步代码全部执行完毕,开始执行微任务,async1 end,then1,then2出队列,打印async1 end,then1,then2
- 微任务执行结束,执行宏任务,set出队列,打印setTimeout
小结
如果这三份面试实战,你都能清楚明白什么时候同步代码执行,什么时候入微任务队列什么时候入宏任务队列,什么时候出微任务队列,什么时候出红任务队列,那么事件循环机制就彻底搞明白了,明白了事件循环机制,你也就能够更加理解js底层V8是如何执行的。
创作不易,烦请一键三连(点赞+收藏)。欢迎私信讨论交流,感谢各位。
转载自:https://juejin.cn/post/7380009750924722202