细聊 JavaScript 的事件执行机制

本文详细探讨了JavaScript的事件执行机制,从线程、进程、单线程和多线程的概念入手,深入讲解同步与异步的区别以及堵塞与非堵塞的定义。文章通过实例解释了JavaScript如何实现异步,并介绍了事件循环(Event Loop)、任务队列(Event Queue)以及在浏览器和Node.js中的不同。此外,还讨论了ES6的作业队列、Node.js的process.nextTick和不同版本的事件循环差异,帮助读者理解JavaScript执行机制的复杂性和重要性。
摘要由CSDN通过智能技术生成

细聊 JavaScript 的事件执行机制

我使用的 Node.js 版本是 v12.13.0 的 , 11 的版本前会与11 版本后的事件循环有些区别。

线程?

都说 JavaScript 是单线程的 ,那么什么是单线程呢?

单线程就是进程只有一个线程。

那这又扯到进程这个概念了。

进程

进程 (Process): 进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。

简而言之,就是存在内存没有执行的程序叫程序,执行起来叫进程

简单说了一下进程,那么就得看看什么是线程了。

线程

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

线程是进程的一个实体,一个进程可以有多个线程。它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

一看更不好理解 ,我的理解是去类比一个活动的执行 ,必然需要一个执行任务的人,或者多个执行任务的人,那么这些人就是线程。也就是活动再细分成任务,那么人就是执行这些任务的基本单位 ,当所有任务完成,也就是活动的完成。

单线程和多线程

那么些微了解了什么是线程,那么就容易理解什么是单线程什么是多线程了。

单线程和多线程都是基于一个进程而言的 。一个进程必须有一个或以上的线程。

单线程进程在执行任务时,只有一个线程可以去执行,并且在同一时刻只能执行一个任务。

而多线程进程在执行任务时是可以同时执行多个任务的。

image-20211103225054658

image-20211103225550974

同步?异步?

同步(sync):也就是同一时间只能做一件事,就拿上面的单线程任务执行过程来看 ,前一个任务完成之前,第二个任务是不会被执行的 。

异步(async) :异步是执行一个任务时,即使这个任务没有结束,也可以继续下去,执行其他任务。

那么根据这两个概念,就可以把任务分成同步任务和异步任务。

同步任务:在任务没有结果前,线程不会继续执行,而是等到当前任务完成后再执行。

异步任务: 执行到此任务时,会把此任务交给其他机制管理 ,而线程去执行其他任务,等到有结果了再返回给线程来继续处理。

对于异步任务的调用,调用的返回并不受调用者控制。

对于通知调用者的三种方式,具体如下:

  • 状态:即监听异步任务的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。

  • 通知:当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。

  • 回调:与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。

同步和异步的区别

同步与异步的区别不在于任务执行的时间。也就是同步任务可能比异步任务执行的时间要长。

主要区别是线程在执行这些任务时,是否需要立即得到结果。需要的是同步,得到结果了才继续执行,而异步是不需要立即得到结果,那么线程可以继续执行也可以等待。

JavaScript 如何做到异步

那么既然 JavaScript 是单线程的,按理说应该是同步的。但同步会遇到一个问题。

就是如果一个JS执行时间太长。就会造成页面不连贯,卡顿。比如我发送了一个异步请求,而请求回来前,我即不能点击其他按钮,也不能滚动页面(也就是我做了这些动作,但是没有响应)。因此体验十分不好。

那么 JS 需要异步,但是执行异步任务,就需要把异步任务交给其他线程。那么如何实现异步呢?

别忘了 JavaScript 代码是在一个 JavaScript 运行时的环境里执行的 。如游览器 ,或者 Node.js 。

他们可以为 JavaScript 提供其他线程 。具体在 JS 执行机制一节会介绍。

堵塞?非堵塞?

堵塞: 是指线程执行任务时,在没有得到结果前,线程会被挂起,不会去执行其他任务了。等得到结果才会继续执行

非堵塞: 是指线程执行任务时,即使不能立即得到结果,该任务也不会影响线程的继续执行其他任务。

同步,异步 与 堵塞,非堵塞的区别

这么看起来 ,堵塞和同步 ,非堵塞和异步似乎很像,有些一模一样了 。但是它们是不一样的。

同步和异步是关于任务的 ,而堵塞和非堵塞是关于线程的。它们有一定的关系,但并不相等。

如何理解呢?

  • 同步和异步是关于任务的 : 比如我有一个快递 ,同步是我自己去做 。由我亲自完成。而异步就是我让我同学帮我拿快递,我不用自己去。
  • 堵塞和非堵塞 : 非堵塞就是 比如我去拿快递,路上我也可以顺便买些东西,只不过会先停止去拿快递。但拿快递并没有堵塞我想做什么 。 而堵塞就比如 我让舍友去拿快递,我有空,但是我什么都不想做,就等快递回来。

如此理解,就是说同步异步的任务不会直接决定线程的堵塞和非堵塞 。

因此我们就有以下4中情况 :

  • 同步堵塞 : 执行一个同步任务,没有结果,不执行其他,线程被挂起。等有结果了再继续执行其他任务

我去买东西,发现没钱了,叫同学发红包,同学还没回,我就付不了。

  • 同步非堵塞 : 任务是同步的 ,但我过程中,暂时停止的这个同步任务,去执行了其他任务,同时又可能时不时回来继续执行。

我不能同时抄作业,又同时打游戏,但我可以打一会,又玩一会。只要我的速度快,你就可能以为我是同时进行的

  • 异步堵塞 : 执行到异步任务时 ,任务不占用当前线程,但是线程的设置机制就是等到任务有结果后再进行执行其他任务

我在打游戏,外卖到了,让舍友拿,但是我好饿,不打了,等外卖

  • 异步非堵塞 : 执行到异步任务时 ,任务交给其他线程执行,当前线程继续执行其他任务。

我在打游戏,让舍友去给我拿外卖,我继续打游戏!

显然异步堵塞几乎没什么用 ,而同步非堵塞也不是很符合实际需求或解决大部分的问题。

JavaScript 的执行机制

先体验一波 JavaScript 的同异步任务的执行结果吧!

console.log(1)

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

console.log(3);
// 1 3 2

在了解同异步前,我会认为答案 是 1 2 3 ,因为定时器是 0 秒的 。应该会被立即执行的吧 。

但实际不是如此的 。这就得说到一种运行机制叫做事件循环机制

事件循环 (Event Loop)

事件循环机制是计算机系统的一种运行机制 。而 JavaScript 是单线程的 ,因此采用了这种机制来解决运行中存在的一些问题。

**先粗浅的看看什么是事件循环 **

单线程无法同一时间执行多个事件 ,必须一件一件的完成 。但如果遇到异步的任务,就比方说点击事件 ,

but.onclick = (){
   }

当线程执行到这段代码时 给节点添加点击事件后,发现 but 节点的点击还没发生 ,也是就等着。那么页面就会出现 “假死” 状态 ,页面操作不了 ,接下来的代码也不执行了 。

而如果使用事件循环机制 ,把异步任务交给其他线程完成 ,那么当执行完成后有结果了再返回主线程。那么在这段本来应该等待的时间里就可以做其他的事情了 。

image-20211104145855582

那么这于多线程有什么区别呢 ? 输入事件回调也会使用到其他线程 ,但其他线程是相当于辅助作用的。主线程遇到异步了就交给它,其他情况下它并不活跃 ,不会占据太多的资源。

而相比多线程 ,多条线程是同地位的 ,这意味着占用的资源更多 ,并且遇到异步任务需要等待时 ,都没有相互帮忙,而是等着 。这显然不合理。

JavaScript 中的事件循环

先看一张图 :

image-20211104173740686

如何理解呢 ? 我们来看下面一段代码 :

console.log('a')  // 函数 1

setTimeout(() => {
     // 函数2 , 回调函数3
    console.log('b')  // 函数4
}, 1000);

console.log('c');  // 函数5

//   a c b

过程 : JavaScript 执行线程按照顺序执行 ,执行函数以时 ,把函数1 压入函数调用栈 。输出 a 。执行完毕弹出。

然后执行到函数2 ,也会被压入函数执行栈,但它是计时器 ,它为异步函数 ,会立即弹出,被放入其他线程中处理,也就是异步处理模块那。

执行函数5 ,同函数1一样,执行完后弹出 。输出 c 。

异步处理模块于主执行线程互不影响,异步函数在进入异步处理模块后根据 FIFO (也就是先进先出,先来后到)的顺序执行 ,一秒后执行完成,将回调函数放入任务队列中 。

此时如果函数执行栈有其他函数 ,则任务队列不会被理睬 ,等到函数执行栈空了,就会监听任务队列,如果有任务就会压入执行栈执行。执行完了就会继续监听任务队列,是否有任务需要执行。如果执行到异步任务,则又放到异步处理模块

那这个重复循环的过程就是事件循环

任务队列(Event Queue)

任务队列有的也叫(消息队列) ,里面的任务 可分为 宏任务(Macro-task)微任务 ( Micro-task ) 。

游览器和 Node.js 在任务队列的不同

游览器和 Node 在宏任务和微任务上是有区别的。主要原因是使用场景不同,提供的 API 也不同 。

游览器 :

macro-task大概包括:

  • setTimeout
  • setInterval
  • I/O
  • UI render

micro-task大概包括:

  • Promise.then(回调)
  • Async/Await(实际就是promise)
  • MutationObserver(html5新特性)

**Node.js **

macro-task 大概包括:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O

micro-task 大概包括:

  • process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)
  • Promise().then(回调)等。

ES6 作业队列

ECMAScript 2015 引入了作业队列的概念,Promise 使用了该队列(也在 ES6/ES2015 中引入)。 这种方式会尽快地执行异步函数的结果,而不是放在调用堆栈的末尾

也就是异步任务也需要排队的 ,也就是如果前面有一个宏任务,就必须等前面一个先执行完。微任务也需要等它前面的微任务先执行完才能被执行。

而 作业队列我们可以理解成微队列 ,因为作业队列必须在同步任务执行完后,但它无需等待宏任务 。因为无论在游览器还是 Node.js ,都会先执行完微任务再执行宏任务 。

console.log(1)

setTimeout(() => {
     
    console.log(2); // 宏任务
}, 0);

new Promise((resolve, reject) =>{
   
    console.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

无糖的酸奶

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

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

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

打赏作者

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

抵扣说明:

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

余额充值