Javascrip 事件循环,就看这个,超详细解读

在说 Js 事件循环前,我们需要先搞清楚一些相关概念,比如:进程,线程(多线程,单线程),主线程,执行栈,同步任务,异步任务(宏任务、微任务),任务队列(宏队列、微队列)。

进程:每个应用程序至少会有一个进程。例如浏览器应用程序,当浏览器启动后,它会在浏览器内部启动多个进程,如GPU进程,网络进程,渲染进程等。每个进程里可以有一个或多个线程。进程之间也是彼此独立,各自占据一个自己的内存空间。

线程:负责处理进程分配的任务,它没有自己的内存空间,但可以共享使用进程的内存空间。如果把电脑操作系统比喻成一个写字楼,一个应用程序就如同写字楼里的一家公司,一个进程就好比公司里的一个部门,而一个线程就是部门里的一个员工,所有的线程(员工)共享一个进程(部门)的内存(资源)。

多线程:就是多个线程同时在处理一个任务,类似公司部门里多个员工分工合作共同去完成同一个项目。

单线程:就是一个任务,只有一个线程在处理,意味着任务多时,就需要排队等候。

JavaScript从诞生开始,最大的特点就是单线程,即,同一个时间只能做一件事。JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

作为单线程语言,JavaScript 的执行顺序为从上到下,且同一时刻只能执行一个任务,这就意味着排队。当遇到一个耗时比较长的任务时,主线程不得不停下来,等到前一个任务执行完之后,再开始执行下一个任务,这样很容易造成阻塞。如果排队是因为计算量大,CPU忙不过来也就算了。但由于类似ajax网络请求、setTimeout时间延迟、DOM事件的用户交互等,这些任务并不消耗 CPU,而是一种空等,白白浪费很多时间,很不合理。

为了保证JavaScript 代码能够高效无阻塞地运行,因此出现了事件循环(Event Loop)

事件循环,包含了 主线程执行栈同步任务异步任务(宏任务微任务)任务队列(宏队列微队列)等。

首先,JavaScript 在执行的过程中,会创建一个JavaScript主线程和一个执行栈,并把所有任务分为同步任务异步任务。同步任务会依次被放入执行栈中,执行栈里的任务(同步任务)以先进先出的方式,被主线程调度执行。异步任务,则会被交给相应的异步模块去处理,一旦异步处理完成后有了结果,就往任务队列的末尾添加注册一个回调事件(回调函数),等待主线程空闲时调用。这样,主线程就只管从执行栈中调度执行同步任务,直到清空执行栈,这时就会去查询任务队列。如果任务队列里有任务,就将排在最前面的任务调入主线程中执行。如果执行时,又有异步任务,就又把异步任务交给异步模块处理,返回结果后,再次往任务队列里注册回调事件。以此类推,形成一个事件环。这样主线程就无需等待,可以不停地执行任务,大大提升了主线程的效率。

主线程:执行JavaScript同步任务的场所。它同一时刻,照顺序每次只能从执行栈中调入一个同步任务并立即执行。

执行栈:存放所有的同步任务,里面的任务以先进先出的方式被主线程调度执行。

同步任务:JavaScript运行后,按照顺序,自动进入执行栈,并依次被主线程调度且立即执行的任务。

console.log(1);
console.log(2);
// console.log() 是同步任务,按照顺序,依次进入执行栈,被主线程调度执行后,依次输出 1,2

异步任务:执行后,存在无法立即处理的任务,主要有setTimeout, setInterval,XHR, Fetch,addEventListener,Promise等。

setTimeout(() => {
    console.log(1)
}, 1000);
// setTimeout是异步任务,执行后,需要等待1000毫秒后,才会输出 1

异步任务又分宏任务微任务。

宏任务:setTimeout, setInterval,XHR, Fetch,addEventListener, 宏任务属于一轮事件循环的开头,在事件循环过程中遇到的宏任务,会被放到下一轮循环去执行。script的整体代码在执行时,就是一个宏任务。

微任务:Promise,是一轮事件循环的最后一个环节,所有的微任务执行完,一轮事件循环就结束了。注*(new Promise() 函数本身是同步任务,只有它的then/catch/finally等方法是微任务)。

setTimeout(() => console.log(1), 0);
Promise.resolve().then(() => console.log(2));
console.log(3);
const p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve();
    }, 1000);
    console.log(4);
});
p.then(() => console.log(5));
// 上述整体代码为一个宏任务开始运行,运行后,JavaScript主线程开始从上到下执行代码
// 第1行代码属于异步任务中的宏任务,其任务类型是延迟任务,放入宏队列中的延迟队列,等候下一轮循环执行
// 第2行代码是一个异步任务中的微任务,放入任务队列中的微队列,等候执行栈空闲后被主线程调用执行
// 第3行代码是同步任务,进入执行栈后,被主线程直接调度且立即执行,输出 3
// 第4行代码,new Promise() 本身属同步任务
// 所以开始执行内部的 setTimeout,发现是个宏任务,放入宏队列,等候下一轮循环执行
// 接着执行同步任务 console.log(4), 直接输出 4
// p.then() 由于 new Promise() 里面的 resolve() 未执行到,目前仍 padding 未完成状态,所以不会被执行
// 此时,执行栈已经清空,主线程开始查询任务队列,发现有一个微任务,调入主线程中执行,于是输出 2
// 至此,本轮事件循环结束,开始第2轮循环,由于第1个 setTimeout 最早在任务队列里注册回调事件,所以先执行
// 于是,在第2轮循环中,console.log(1) 作为一个同步任务,被主线程直接调用执行,输出 1
// 接着开始第3轮循环,执行第2个 setTimeout 里的 resolve(),此时 p 的状态变成了已完成,开始调用then()方法
// p.then() 是一个微任务,于是被放入微队列,但当前循环的执行栈里没有任务,该微任务直接被调入主线程执行,输出 5
// 最终,上述代码的执行结果为:3-4-2-1-5

任务队列:存放着所有异步任返回结果后所注册的回调事件,等待执行栈空闲时主线程的调用。过去把任务队列分为宏队列微队列,这种说法目前已经无法满足复杂的浏览器环境,取而代之的是一种灵活多变的处理方式。

根据 W3C 官方的解释,不同的任务可以有不同的类型,分属不同队列,不同的队列有不同的优先级,而同类型的任务必须放在同一个队列。在一个事件循环中,必须有一个微队列,相对于其他队列,微队列具有最高的优先级,必须优先调度执行。其他队列则由浏览器自行决定取哪一个队列的任务执行。

setTimeout, setInterval 的任务需要等待计时完成后再执行,属于延迟队列

XHR, Fetch 的任务需要等待网络通信完成后再执行,属于通信队列

addEventListener 的任务需要等待用户操作后再执行,属于交互队列

Promisethen/catch/finally 的任务为微任务,属于微队列

一般的队列优先情况是: 微队列 > 交互队列 > 延迟队列

来看个例子,下面的代码会输出什么?

console.log(1);

setTimeout(() => {
    fn(2);
    console.log(3);
    setTimeout(() => console.log(4), 1000);
}, 0);

Promise.resolve().then(() => {
    console.log(5);
});
fn(6);

function fn(x) {
    console.log(x);
}

// 第一步,执行 console.log(1),这是一个同步任务,直接输出 1;
// 第二步,执行 setTimeout,这是一个这是一个宏任务,留到下一次循环再执行,主线程继续往下执行;
// 第三步,执行 Promise,这也是一个异步任务,交给异步模块处理,继续往后执行;
// 第四步,执行 fn(6),这是一个同步任务,直接执行,输出6;
// 此时,执行栈已经清空没任务了,主线程处于空闲状态,于是就去查询任务队列;
// 发现任务队列的微队列里有一个微任务 Promise.resolve().then(), 于是调入主线程执行,输出5;
// 至此,第一轮事件循环结束。
// 第五步,开始第二轮事件循环,此时,任务队列的延迟队列里有一个 setTimeout 任务,执行它;
// 于是,第六步,执行 fn(2),函数里面的 console.log(x)是一个同步任务,直接输出 2;
// 第六步,执行 console.log(3),同步任务,直接输出 3;
// 第七步,又是一个 setTimeout 宏任务,下一循环执行,第二轮事件循环结束。
// 开始第三轮循环,此时,只有一个 console.log(4) 的同步任务,直接输出 4;
// 最终,上述代码的执行结果为:1-6-5-2-3-4

再看一个例子。

const p = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(1);
    }, 1000);

    console.log(2);

    setTimeout(() => {
        Promise.resolve().then(() => {
            console.log(3);
        });
        console.log(4);
        resolve(5);
    }, 0);
});

p.then(v => console.log(v)).catch(r => console.log(r));

console.log(6);

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

// 第一步,执行 new Promise(),注意,new Promise() 本身是一个同步任务于是开始执行 Promise 函数内部的第一个 setTimeout 任务,这个一个宏任务,1000毫秒后会在任务队里注册一个的回调事件a
// 第二步,执行 console.log(2) ,这是一个同步任务,直接输出 2
// 第三步,执行第二个 setTimeout 宏任务,该任务会立即在任务队列里注册一个回调事件b
// 第四步,由于 Promise 目前还仍然处于padding 状态,所以,p.then() 不会执行
// 第五步,执行 console.log(6),直接输出 6
// 第六步,执行一个0秒的 setTimeout 宏任务,立即在任务队列里注册了一个回调事件 c 
// 至此,执行栈已经清空,微队列里没东西,任务队列里有3个宏任务,其顺序为 b, c, a,第一个事件循环结束
// 于是,第二个事件循环开始,主线程开始执行宏任务b
// 第七步,执行任务 b 的 Promise,在微队列里注册了一个微任务
// 第八步,执行 console.log(4),这是一个同步任务,直接输出 4
// 第九步,执行 resolve(5),这是一个同步任务,此时直接将 Promise 的状态变成fulfilled(已成功)
// 此时 p.then(...) 会自动被调用,在微队列里注册了第二个微任务
// 第十步,此时第一个宏任务 b 的执行栈已清空,主线程开始查询任务队列,发现微队列里有两个微任务,开始执行微任务
// 第十一步,执行第一个微任务,输出 3
// 第十二步,执行第二个微任务,输出 5
// 第十三步,执行第二个宏任务 c,输出 7
// 第十四步,执行第三个宏任务 a,输出 1
// 最终,上述代码的执行结果为:2-6-4-3-5-7-1

个人理解,如有说的不对的地方,欢迎留言指正。

参考资料 https://www.ruanyifeng.com/blog/2014/10/event-loop.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaScript事件循环JavaScript中用于处理异步操作的机制。由于JavaScript是单线程的,它无法同时执行多个任务。为了处理异步任务,JavaScript引入了事件循环机制。 事件循环主要由以下几个组成部分: 1. 调用堆栈(Call Stack):用于存储函数调用的记录。当函数被调用时,会将其压入调用堆栈中,当函数执行完毕后,会从调用堆栈中弹出。 2. 任务队列(Task Queue):用于存储待处理的异步任务。当异步任务完成时,会被添加到任务队列中。 3. 事件循环(Event Loop):负责监听调用堆栈和任务队列的状态,当调用堆栈为空时,会从任务队列中取出任务并执行。 事件循环的工作流程如下: 1. JavaScript引擎执行同步任务,并将异步任务添加到任务队列中。 2. 当调用堆栈为空时,事件循环会从任务队列中取出一个任务。 3. 取出的任务会被压入调用堆栈中执行。 4. 执行完毕后,调用堆栈为空,事件循环继续从任务队列中取出任务执行。 这样循环执行,实现了异步任务的处理。 相关问题: 1. 什么是异步任务?为什么需要处理异步任务? 2. JavaScript如何处理异常? 3. JavaScript中的回调函数是什么?如何使用回调函数处理异步任务? 4. 什么是Promise?如何使用Promise处理异步任务? 5. JavaScript中的定时器有哪些?它们是如何工作的? 6. 什么是事件驱动编程?如何在JavaScript中使用事件驱动编程? 7. JavaScript中的Event Loop与浏览器的渲染机制有什么关系?<span class="em">1</span><span class="em">2</span> #### 引用[.reference_title] - *1* [详解JavaScript事件循环机制](https://download.csdn.net/download/weixin_38694006/13608655)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [浏览器显示数据库中数据的条形图柱状图 前后端分离vue.js+spring boot 计算机软件工程课程设计毕业设计 ...](https://download.csdn.net/download/Amzmks/88275824)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值