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中的call和apply
javascript中的call和apply
|
2月前
|
消息中间件 Web App开发 JavaScript
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
70 0
|
20天前
|
开发框架 JavaScript 前端开发
描述JavaScript事件循环机制,并举例说明在游戏循环更新中的应用。
JavaScript的事件循环机制是单线程处理异步操作的关键,由调用栈、事件队列和Web APIs构成。调用栈执行函数,遇到异步操作时交给Web APIs,完成后回调函数进入事件队列。当调用栈空时,事件循环取队列中的任务执行。在游戏开发中,事件循环驱动游戏循环更新,包括输入处理、逻辑更新和渲染。示例代码展示了如何模拟游戏循环,实际开发中常用框架提供更高级别的抽象。
11 1
|
24天前
|
JavaScript 前端开发
js开发:请解释什么是事件委托(event delegation),并给出一个示例。
事件委托是JavaScript中优化事件处理的技术,通过绑定事件处理器到共享的父元素,利用事件冒泡机制来处理子元素的事件。这种方法能提升性能、简化代码并降低内存消耗。示例展示了如何在父元素上监听`click`事件,然后通过`event.target`识别触发事件的具体子元素(如`<li>`),实现对动态生成列表项的点击事件处理。
|
25天前
|
消息中间件 前端开发 JavaScript
深入理解JavaScript中的事件循环机制
JavaScript作为一种前端开发必备的编程语言,在处理异步操作时常常涉及到事件循环机制。本文将深入探讨JavaScript中事件循环的工作原理,帮助读者更好地理解和运用这一关键概念。
|
1月前
|
JavaScript
JS中call()、apply()、bind()改变this指向的原理
JS中call()、apply()、bind()改变this指向的原理
|
1月前
|
前端开发 JavaScript UED
描述 JavaScript 中的事件循环机制。
描述 JavaScript 中的事件循环机制。
10 1
|
2月前
|
消息中间件 存储 前端开发
理解JavaScript事件循环机制
理解JavaScript事件循环机制
16 0
|
3月前
|
Web App开发 JavaScript 前端开发
了解 Node.js 的运行机制:从事件循环到模块系统(下)
了解 Node.js 的运行机制:从事件循环到模块系统(下)
了解 Node.js 的运行机制:从事件循环到模块系统(下)
|
3月前
|
JavaScript 前端开发 数据挖掘
了解 Node.js 的运行机制:从事件循环到模块系统(上)
了解 Node.js 的运行机制:从事件循环到模块系统(上)
了解 Node.js 的运行机制:从事件循环到模块系统(上)