JS (Event Loop)事件循环 和 (Call Stack) 调用堆栈

简介: • 1.JS如何在浏览器中运行• 调用栈• 堆栈溢出• Web APIs• 回调队列• 事件循环• setTimeout(fn,0)• 工作队列和异步代码• Promises• promises适合在哪里


该文章是以国外一篇文章,关于JS事件轮训和调用栈(JavaScript Event Loop And Call Stack Explained)为基础。同时加上其他资料的所编写的。

如果想直接根据原文学习,可以忽略此文。但是不要忘记点赞+关注。

如果你觉得可以,请多点赞,鼓励我写出更精彩的文章🙏。
如果你感觉有问题,也欢迎在评论区评论,三人行,必有我师焉


概要


JS如何在浏览器中运行

要了解一件事本来的面貌特征,就需要一种高屋建瓴的方式来从整体把握整个事情的来龙去脉和发展脉络。

而如下的图片就展示了,JS是如何在浏览器中运行的轨迹。

而需要注意的一点:在图片中大部分所涉及的数据结构和部件都不是JS所提供的。

Web APIs回调队列事件循环都是浏览器友情赞助的。

调用栈

在我们第一次接触JS的时候,无论从课本上还是其他辅助资料,首先眼入眼帘的概念就是: JS是一个单线程语言。 那这个单线程又是在何种 限定条件 下才会这么说呢?

JS在某个时刻只能做一件事,因为JS 有且仅有 一个调用栈。

调用栈是帮助 JS编译器 用于追踪函数被调用的动向(顺序)的一种机制。

script 或者函数调用另外一个函数的时候,被调用的函数就会被 添加到调用栈的顶部。(借用数据结构中的知识来讲,就是讲数据入栈)。

当函数退出(执行return或者到函数最底部),编辑器就讲该函数从调用栈顶部移除。

让我们通过一个简单的例子,来跟踪一下,函数调用是如何和调用栈合作的。

const addOne = value => value + 1;
const addTwo = value => addOne(value + 1);
const addThree = value => addTwo(value + 1);
const calculation = () => addThree(1) + addTwo(2);
// 函数调用
calculation();
复制代码

每当一个函数调用(A)另外一个函数 (B),被调用函数被 push到调用栈的顶部(在刚才调用函数(A)的上面)。

调用栈处理被调用函数的顺序遵循 LIFO (Last in,First Out)原则。

上面的示例,按照如下步骤进行操作:

  1. 1. 该文件被加载并且mian函数(代表整个文件)被调用,并且该函数被 push 到调用栈中。(此时,该函数为栈底也为栈顶)
  2. 2. mian函数调用calculation(),同理,该函数也被 push 到栈顶。
  3. 3. calculation()函数调用addThree(),同理,该函数也被 push 到栈顶。

....重复操作,直到调用addOne()

  1. 6.  addOne()没有调用任何函数。所以当 addOne()退出(执行return或者到函数最底部),它将会从调用栈-栈顶被 pop 出。
  2. 7. 在addtTwo()中,处理addtOne()的返回结果,也经历着同 addtOne() 一样的命运-- 退出(执行return或者到函数最底部),它将会从调用栈-栈顶被 pop 出。

........重复操作,直到调用calculation()

  1. 14. 此时在该文件中不存在被调用的语句或者函数。所以main()执行 退出操作。
我们上文中,将函数执行环境称为 main(),其他在浏览器中,它是一个匿名函数- anonymous

堆栈溢出

在我们平时,开发代码的时候,总是一个不小心,就会产生了 死循环,而产生死循环的原因很简单,就是某一个函数或者代码,总是不停的迭代循环-无穷无尽,有一种到黄河不死心的决心和毅力。

而我们通过上文介绍的 调用栈的运行机制得知,当存在函数调用的时候,被调用的函数就会被 push到调用栈-栈顶。而我们知道,无论何种语言,针对某一个数据都存在一个 MAX存储空间。

所以,可想而知,堆栈溢出,就是因为被调用函数无穷无尽的被 push到 调用栈,在某个时刻,被加入的函数,超过了栈的容忍上限

所以,浏览器会给你一个提示:

Uncaught RangeError: Maximum call stack size exceeded
复制代码

我们通过错误信息的提示可知。它表示,是函数调用导致了这个错误。在这个例子中,错误发生在函数b中,而b又被函数a调用。

如果你在开发中遇到该错误,你通过错误信息的展示会发现,某一个函数被调用了很多次。调用栈最大范围为  10,000- 50,000 次。 如果你的函数被调用这么多次,也就意味着你代码中存在一个死循环

浏览器通过限定调用栈的存储大小来避免你无意中写出的死循环从而导致页面挂掉

总结: 调用栈用于 跟踪你代码中函数调用的顺序。它遵循 LIFO(后进先出)的原则---意味着,它总是处理被存放在栈顶的函数。

JS有且仅有一个调用栈--->意味着JS在某一个时刻只能做一件事。


Web APIs

从上文所了解的概念得知,JS在某一个时刻只能做一件事。

但是这有一个很大的限定条件--->就是该条规则只适用于在了JS范围内。而这个条件一旦扩大,扩大到浏览器范围内,就会产生不一样的效果。在浏览器范围内,在JS运行的同时,也可以通过浏览器提供的API并行做其他的事情。

浏览器为我们提供了,一些可以在JS代码中调用的API。然后,这些API的执行是由平台(浏览器,Node...)所控制的,这也就是为什么这些API不会阻塞调用栈的执行。

另外一个优点就是,这些API是由底层语言(C语言)所写,它们能做JS所不能做的事情。

这些API能赋予你所写的JS代码进行 AJAX请求操作DOM访问local storage使用worker等等。


回调队列

通过使用浏览器为我们提供的API,我们能轻松的实现--->在JS运行的同时,做其他额外的事。但是,该如何在我们维护的JS代码中获取并使用Web API返回的结果?

此时,callback(回调函数)粉墨登场。通过callback,web API允许我们在API调用并且返回对应结果后触发指定的回调函数。

callback 是个啥? callback 是一个 做为参数被传入到其他函数的 函数。callback一般在调用函数被执行完,才被执行。 高阶函数:接收一个函数作为参数的函数
const a = () => console.log('a');
const b = () => setTimeout(() => console.log('b'), 100);
const c = () => console.log('c');
a();
b();
c();
复制代码

由于setTimeout属于web API。 所以在执行到setTimeout的时候,触发了web API的执行流程。而此时JS解释器 继续 执行剩下的语句。

timeout已经到了并且调用栈为空,被传入到setTimeout的回调函数被执行。

输出结果如下:

a
c
b
复制代码

什么是回调队列

虽然setTimeout被执行,但是它的回调函数不会立即被调用。

还记得JS在某一个时刻只能做一件事吗?

我们将一个函数作为参数传入到setTimeout中,其作为setTimeout的回调函数是用JS所写的。因此,JS解释器需要运行这段代码,也就意味着,这段代码需要被 push到调用栈中。而满足被push的条件就是调用栈是的。所以,我们需要等待。

调用setTimeout,触发了web API的执行,然后将callback(回调函数) enqueue(入队)。 当调用栈为时,event loop(事件循环)从callback queue中dequeue出刚才被入队的callback,并将其 push到调用栈中。

和调用栈不同的是,回调队列遵循 FIFO(First In, First Out),意味着处理回调的顺序是按照入队的顺序来进行。


事件循环

JS事件循环总是取出callback queue队首的元素,并且在call stack为空的时候,将其 pushcall stack中。

JS代码遵循一种从一而终的运行模式,也就是说,如果call stack中存在代码,事件循环将会被阻塞直到call stack为空的时候,才会再次执行 --->从callback queuedequeue --->pushcall stack的流程。

不要通过运行计算密集型任务来阻塞调用堆栈。 如果在自己维护的代码中执行过多的耗时代码,此时,事件循环一直处于阻塞状态,网站将会变得卡顿。

setTimeout

如果我们想在不阻塞主线程太久的情况下执行某些任务,我们可以利用上面描述的行为。

将异步代码放在回调中并将setTimeout设置为0ms,该操作将允许浏览器在继续执行回调之前执行诸如更新DOM之类的操作。


工作队列和异步代码

除了callback queue,浏览器中还存在另外一个用于专门接收promises的队列 ---job queue(工作队列)

Promises

ES6第一次引入promises,它可以通过Babel转换被主流浏览器所识别和使用。

promises是另外一种不同于使用callback处理异步代码的方式。它们允许你轻松地将异步函数链在一起,而不会在所谓的回调地狱中结束。

setTimeout(() => {
  console.log('Print this and wait');
  setTimeout(() => {
    console.log('Do something else and wait');
    setTimeout(() => {
      // ...
    }, 100);
  }, 100);
}, 100)
复制代码

如果存在很多异步依赖,那就会陷入到回调地狱中。

而使用promises,上述代码就会变得更加可读。

const timeout = (time) => new Promise(resolve => setTimeout(resolve, time));
timeout(1000)
  .then(() => {
    console.log('Hi after 1 second');
    return timeout(1000);
  })
  .then(() => {
    console.log('Hi after 2 seconds');
  });
复制代码

如果使用ES7的async/await语法,更加简明。

const logDelayedMessages = async () => {
  await timeout(1000);
  console.log('Hi after 1 second');
  await timeout(1000);
  console.log('Hi after 2 seconds');
};
logDelayedMessages();
复制代码

promises适合在哪里

promises的行为与回调略有不同,因为它们有自己的队列。

工作队列,也被称为promise队列,该队列拥有比callback queue更高的优先级

也就意味着,event loop会优先遍历promise queue,在promise queue为空的时候,才会遍历 callback queue

console.log('a');
setTimeout(() => console.log('b'), 0);
new Promise((resolve, reject) => {
  resolve();
})
.then(() => {
  console.log('c');
});
console.log('d');
复制代码

对应的输出结果为

a
d
c
b
复制代码



相关文章
|
1月前
|
存储 JavaScript 前端开发
深入理解JavaScript中的事件循环(Event Loop):机制与实现
【10月更文挑战第12天】深入理解JavaScript中的事件循环(Event Loop):机制与实现
79 3
|
22天前
|
JavaScript 前端开发 开发者
JavaScript的事件循环
【10月更文挑战第27天】理解JavaScript的事件循环机制对于正确编写和理解JavaScript中的异步代码至关重要,它是JavaScript能够高效处理各种异步任务的关键所在。
31 1
|
1月前
|
JavaScript 前端开发
JS高级—call(),apply(),bind()
【10月更文挑战第17天】call()`、`apply()`和`bind()`是 JavaScript 中非常重要的工具,它们为我们提供了灵活控制函数执行和`this`指向的能力。通过合理运用这些方法,可以实现更复杂的编程逻辑和功能,提升代码的质量和可维护性。你在实际开发中可以根据具体需求,选择合适的方法来满足业务需求,并不断探索它们的更多应用场景。
10 1
|
1月前
|
前端开发 JavaScript
深入理解JavaScript中的事件循环(Event Loop):从原理到实践
【10月更文挑战第12天】 深入理解JavaScript中的事件循环(Event Loop):从原理到实践
36 1
|
1月前
|
Web App开发 JavaScript 前端开发
深入理解Node.js事件循环和异步编程模型
【10月更文挑战第9天】在JavaScript和Node.js中,事件循环和异步编程是实现高性能并发处理的基石。本文通过浅显易懂的语言和实际代码示例,带你一探究竟,了解事件循环的工作原理及其对Node.js异步编程的影响。从基础概念到实际应用,我们将一步步解锁Node.js背后的魔法,让你的后端开发技能更上一层楼!
|
1月前
|
设计模式 JavaScript API
Node.js 事件循环
10月更文挑战第3天
30 0
Node.js 事件循环
|
1月前
|
JavaScript 前端开发
js 中call()和apply()
js 中call()和apply()
28 1
|
1月前
|
JavaScript 调度 数据库
深入浅出:Node.js中的异步编程与事件循环
【9月更文挑战第30天】在Node.js的世界里,理解异步编程和事件循环是掌握其核心的关键。本文将通过浅显易懂的语言和实际代码示例,带你探索Node.js如何处理并发请求,以及它是如何在幕后巧妙地调度任务的。我们将一起了解事件循环的各个阶段,并学会如何编写高效的异步代码,让你的应用程序运行得更加流畅。
60 10
|
1月前
|
JavaScript 前端开发 开发者
深入理解Node.js中的事件循环和异步编程
【9月更文挑战第31天】本文旨在揭示Node.js背后的强大动力——事件循环机制,并探讨其如何支撑起整个异步编程模型。我们将深入浅出地分析事件循环的工作原理,以及它如何影响应用程序的性能和稳定性。通过直观的例子,我们会展示如何在实际应用中利用事件循环来构建高性能、响应迅速的应用。此外,我们还会讨论如何避免常见的陷阱,确保代码既优雅又高效。无论你是Node.js的新手还是经验丰富的开发者,本篇文章都将为你提供宝贵的洞察和实用技巧。
64 6