切换语言为:繁体

通过实例理解 JavaScript 事件循环机制(Event Loop)

  • 爱糖宝
  • 2024-06-14
  • 2173
  • 0
  • 0

前言

相信大家都听过一句话,js是单线程的语言,那么这句话如何理解呢?今天就来讨论一下js中的事件循环,理解完事件循环机制,你就会对这句话有更深刻的理解。

事件循环(event loop)

同步与异步代码

let a = 1
console.log(a);

setTimeout(function () {
    a++
}, 1000)

console.log(a);


问:a打印多少? 答:1,1。因为定时器是一个耗时的代码,同步代码是不需要耗时执行的,异步代码是需要耗时执行的,JSV8引擎会先执行同步代码再执行异步代码

到这,你已经了解了什么是同步代码什么是异步代码。

  • 同步代码:不需要耗时执行的代码。

  • 异步代码:需要耗时执行的代码。

  • JSV8引擎会先执行同步代码再执行异步代码。

进程与线程

大家大学阶段应该都学习过一门课程叫做《操作系统》,这门课程对进程与线程都有深刻的讲解。

  • 进程:是操作系统中程序的一次执行过程,是资源分配的基本单位。它拥有独立的地址空间、内存、文件等资源。

  • 线程:是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。线程共享所属进程的资源,但也有自己少量的私有资源,如程序计数器、栈等。

  • 多个线程可以在一个进程内并发执行,共同完成进程的任务,提高系统的并发性和效率。

    • 例如:一个浏览器的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打造之初就是想作为一个浏览器的脚本语言,因此如果它占用用户过多的性能,这门语言会被嫌弃。

  1. 节约性能:不需要过多考虑多线程环境下资源竞争、死锁等复杂情况,代码逻辑相对简单清晰,降低了开发难度和出错概率。

  2. 节约上下文切换的时间:例如有一个循环语句要执行1s,然后对a变量做一个操作,一个定时器也要执行1s,也对a变量做一个操作,多线程语言就会对其中一个上锁,先执行完另一个然后切换回这个解锁。这就存在上下文切换的耗时。

  3. 便于与浏览器交互:能更好地与浏览器的单线程模型相契合,确保页面渲染和用户交互的稳定性,不会因为多线程竞争而导致页面显示异常等问题。

微任务与宏任务

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. 执行同步代码(这属于是宏任务)

  2. 同步执行完毕后,检查是否有异步需要执行

  3. 如果有,则执行微任务

  4. 微任务执行完毕后,如果有需要就会渲染页面

  5. 执行异步宏任务,也是开启下一次事件循环(因为宏任务中,也一样会有同步代码、异步代码...)

面试题实战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. 第一行代码,执行打印1(同步代码)

  2. 第二行代码promise的调用,同步代码,打印2

  3. 第六行代码是异步代码里面的微任务,因此,加入微任务队列挂起(then1)。

  4. 第九行代码是异步代码里面的微任务,因此,加入微任务队列挂起(then2)。

  5. 第十二行代码是异步代码里的宏任务,因此,加入宏任务队列挂起(set1)。

  6. 第十五行代码是同步任务,打印6。

  7. 至此,第一次事件循环机制里的第一步,同步任务全部执行完毕,开始寻找是否有异步代码需要执行

  8. 如果有,执行微任务,因此then1出队列,也就是打印3,然后then2出队列,打印4。

  9. 微任务执行完毕后,没有发现渲染操作,因此接下来执行异步中的宏任务,也是开启了下一次事件循环,set1出队列,执行打印5.

  10. 最终结果:1,2,6,3,4,5。

通过实例理解 JavaScript 事件循环机制(Event Loop)

面试题实战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. 第一行执行,同步代码,打印1

  2. new了一个Promise,构造函数本身也不是异步,同步代码,执行打印2

  3. 调用resolve,.then就能执行了,发现是一个异步的微任务,因此入微任务队列挂起then1(打印3挂起)

  4. 到达十二行,发现是一个定时器,异步里面的宏任务,入宏任务队列set1

  5. 18行同步代码,打印7

  6. 至此,第一次事件循环的第一步同步代码执行结束

  7. 执行微任务,then1出队列,then1中有同步和异步,同步先执行,因此打印3,然后发现有个定时器,因此set2入宏任务队列。微任务执行结束

  8. 开始执行宏任务,set1出队列,宏任务开启一次新的事件循环,同步任务先执行,打印5,然后第二次事件循环发现了一个定时器宏任务,set3入宏任务队列,第二次事件循环的同步结束,然后去找微任务队列,发现微任务队列是空的,紧接着去宏任务队列找宏任务,开启第三次事件循环,set2出队列,因此打印4,这也意味着第二次事件循环宏任务结束,第二次事件循环结束,打印4即是第二次时间循环的结束也是第三次事件循环的开始,紧接着去微任务队列找,发现没有,然后去宏任务队列找,set3出队列,打印6。

  9. 因此结果: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会将后续代码阻塞进微任务队列

  1. 第一行同步代码执行 打印script start

  2. 第10行带了了async1的调用,async2前面有await,因此async2调用,async2里面是同步,打印async2 end,然后轮到第五行代码执行,但是由于await将这行打印放到了微任务队列,因此async1 end入微任务队列

  3. 到达第十一行,是一个定时器,宏任务进宏任务队列set

  4. 达到第十四行,同步任务打印promise

  5. 到达第十八行,进微任务队列then1

  6. 达到21行,进微任务队列then2(目前微任务队列有3个,一个async1 end,then1,then2)

  7. 24行同步任务,打印script end

  8. 至此同步代码全部执行完毕,开始执行微任务,async1 end,then1,then2出队列,打印async1 end,then1,then2

  9. 微任务执行结束,执行宏任务,set出队列,打印setTimeout

小结

如果这三份面试实战,你都能清楚明白什么时候同步代码执行,什么时候入微任务队列什么时候入宏任务队列,什么时候出微任务队列,什么时候出红任务队列,那么事件循环机制就彻底搞明白了,明白了事件循环机制,你也就能够更加理解js底层V8是如何执行的。

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.