前言
相信大家都听过一句话,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是单线程的
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是如何执行的。