🔥🔥🔥初探浏览器事件循环是如何工作的

"浏览器事件循环是如何工作的?" 这个问题几乎在每次面试中都会被问到。面试官经常出一下谁先执行谁先输出的代码题来恶心人!对事件循环的深入了解可以让你了解浏览器将如何处理代码,以及如何不做无用功,而是帮助其提高效率,以便应用程序的用户获得最佳体验。

事件循环类似于项目管理器。它协调事件、渲染、网络请求、用户交互和所有异步代码。

接下来我们测验一些挑战。面试官喜欢问这样的问题。还请各位同学自行检查。如果不明白这段代码是如何工作的,请不要担心,我将在本文中进一步讲解。

1. 测试题

1. 第一题

这是演示事件循环如何工作的最简单的示例,这也是最常见的面试题。

js 复制代码
setTimeout(() => console.log(1), 0);

console.log(2);

new Promise(res => {
  console.log(3)
  res();
}).then(() => console.log(4));

console.log(5);

2. 第二题

js 复制代码
setTimeout(() => {
  console.log('timeout 1');
  const promise = new Promise((res) => res());
  promise.then(() => console.log('promise 1'));
});

setTimeout(() => console.log('timeout 2'));

const promise = new Promise((res) => res());

promise.then(() => console.log('promise 2'));

3. 第三题

js 复制代码
setTimeout(() => console.log('timeout 1'));

const promise = new Promise(resolve => setTimeout(resolve));

promise.then(() => console.log('promise 1'));

Promise.resolve().then(() => console.log('promise 2'));

setTimeout(() => console.log('timeout 2'));

为了理解这些示例中的代码,让我们更深入地了解一下事件循环。

2. 事件循环是如何工作的

事件循环对宏任务和微任务队列进行操作。

宏任务(和微任务)是浏览器需要完成的一项工作。一般来说,我们可以说任务是一个函数。

任务示例:

  • 脚本执行
  • 解析html
  • 设置超时回调
  • 鼠标单击或任何其他事件的回调

微任务是脚本执行后需要立即执行的任务。

微任务示例:

我们将研究事件循环的算法,并通过一个图形示例来帮助我们清楚地了解。

考虑以下等待执行的任务:

为了简化,事件循环无限期地执行以下步骤:

  1. 如果任务队列中至少有一个任务,则获取第一个任务并执行它。这就是bar()我们例子中的函数。
  1. 如果微任务队列中有任务,则从最早的任务开始执行所有任务。函数qqz(),然后是baz()函数。
  1. 更新渲染图,最后任务队列为空。

3. 调用堆栈

当任务被执行时会发生什么?它被推入调用堆栈。让我们看一个例子。

js 复制代码
function bar() {
  new Promise((resolve) => resolve())
    .then(() => console.log('promise'));
}

function foo() {
  setTimeout(() => console.log('timeout'));
  bar();
}

foo();

浏览器面临的第一个任务是执行脚本

浏览器执行的任务进入调用堆栈。

此代码调用的所有函数也会进入调用堆栈。

如果函数执行后创建了一个新任务,则该任务将被放入任务队列中。

当一个函数被执行时,它会从栈中弹出。(setTimeout在我们的例子中)

在"堆栈"数据结构中------最后一个进,最后一个出。在"队列"数据结构中,情况正好相反------先进先出。

如果函数执行结果创建了一个新的微任务,它将被放入微任务队列中。

接下来,Promise()bar()foo()、 和script依次从堆栈中弹出。

当堆栈变空时,任务完成。之后,就轮到执行任务执行过程中积累的所有微任务了。

当堆栈为空并且队列中没有微任务时,浏览器可以继续执行下一个宏任务。

当两个队列都为空时,浏览器不执行任何操作,等待新任务。

现在我们可以回到我们的测试题中,看看浏览器在执行它们时将经历哪些步骤。

4. 讲解测试题

1. 第一题

js 复制代码
setTimeout(() => console.log(1), 0);

console.log(2);

new Promise(res => {
  console.log(3)
  res();
}).then(() => console.log(4));

console.log(5);

浏览器步骤:

  1. 第一个要执行的任务是脚本。这一步的结果:
  • 回调setTimeout被添加到任务队列中。
  • 执行promise器函数res => { console.log(3); res(); }是同步执行的。所以,这一步的输出:2, 3, 5
  • 回调promise被添加到微任务队列中。
  1. 执行微任务队列中的任务。输出:4.

  2. 执行最后一个任务------setTimeout回调。输出:1.

结果:2, 3, 5, 4, 1.

2. 第二题

js 复制代码
setTimeout(() => {
  console.log('timeout 1');
  const promise = new Promise((res) => res());
  promise.then(() => console.log('promise 1'));
});

setTimeout(() => console.log('timeout 2'));

const promise = new Promise((res) => res());

promise.then(() => console.log('promise 2'));

浏览器步骤:

  1. 执行脚本。这一步的结果:
  • 两个setTimeout回调都按照它们在代码中出现的顺序添加到任务队列中。
  • 第二个回调promise被添加到微任务队列中。
  1. 执行微任务队列中的任务。输出:promise 2.

  2. 执行宏任务队列中最早的任务。这一步的结果:

  • 输出:timeout 1.
  • 新的promise回调被添加到微任务队列中。
  1. 执行微任务队列中的任务。输出:promise 1.

  2. 执行最后一个宏任务。输出:timeout 2.

结果:promise 2, timeout 1, promise 1, timeout 2.

3. 第三题

js 复制代码
setTimeout(() => console.log('timeout 1'));

const promise = new Promise(resolve => setTimeout(resolve));

promise.then(() => console.log('promise 1'));

Promise.resolve().then(() => console.log('promise 2'));

setTimeout(() => console.log('timeout 2'));

浏览器步骤:

  1. 执行脚本。这一步的结果:
  • timeout 1并将timeout 2回调添加到任务队列中。
  • 执行promise器函数resolve => setTimeout(resolve)是同步执行的。setTimeout于是任务队列中又添加了一个。
  • Promise.resolve()立即解决承诺,其回调被添加到微任务队列中。因为第一个promise尚未完成,所以它的回调尚未添加到微任务队列中。
  1. 执行微任务队列中的任务。输出:promise 2.

  2. 从任务队列中执行任务,从最旧的任务开始。输出:timeout 1.

  3. 由于微任务队列为空,因此再次执行任务队列中最旧的任务。结果,() => console.log('promise 1')函数被添加到微任务队列中。

  4. 执行微任务队列中的任务。输出:promise 1.

  5. 执行任务队列中的任务。最后我们会timeout 2在控制台看到。

结果:promise 2, timeout 1, promise 1, timeout 2.

5. 渲染测试题

1. 测试1

尝试猜测屏幕上会显示什么:

js 复制代码
setTimeout(() => {
  console.log('timeout 1');
  document.body.innerHTML = 'A';
});

requestAnimationFrame(() => {
  console.log('requestAnimationFrame');
  document.body.innerHTML = 'B';
});

setTimeout(() => {
  console.log('timeout 2');
  document.body.innerHTML = 'C';
});

console.log('script');
document.body.innerHTML = 'D';

根据MDN

window.requestAnimationFrame()方法告诉浏览器您希望执行动画,并请求浏览器在下一次重绘之前调用指定的函数来更新动画。

换句话说,该requestAnimationFrame()方法在渲染步骤之前被调用。

宏任务(以及所有微任务)执行后,浏览器会更新渲染。

因此,第一次渲染预计在脚本执行后进行。然后屏幕上显示字母的顺序如下:B, A, CD永远不会被绘制到屏幕上,因为它与requestAnimationFrame回调共享一个渲染步骤,该回调将最后执行。

因此,用户将C在屏幕上看到。这就是这段代码现在在 Safari 中的工作方式。

但是,如果浏览器认为渲染步骤不会包含视觉变化,或者为了优化目的而想要组合多个任务,则浏览器可能会跳过渲染步骤。例如,浏览器可能决定将多个计时器回调合并在一起并跳过其间的渲染。

因此,执行此示例的大多数现代浏览器将在执行脚本后跳过渲染并捆绑setTimeout回调。因此,三项任务将只有一次渲染。这个小小的遗漏将极大地改变最终结果。浏览器将呈现的唯一内容是B.

要验证这一点,请运行代码,打开 chrome Dev Tools->Performance选项卡 -> 单击Record,刷新页面。您只会看到已绘制的一帧。

这种不一致的结果不仅可能出现在浏览器之间,而且可能出现在同一浏览器从启动到启动的整个过程中。因此,应该避免这种不明确的代码。

2. 测试2

在下面的代码中,有两个按钮可以使徽标旋转 360 度。一个按钮使用 进行动画处理setTimeout,另一个按钮使用 进行动画处理requestAnimationFrame。您认为哪种动画可以更快地将徽标旋转 360 度?

js 复制代码
const animationBtn = document.getElementById('animation-btn');
const timeoutBtn = document.getElementById('timeout-btn');
const logo = document.getElementById('logo');

animationBtn.addEventListener('click', () => animate(requestAnimationFrame));
timeoutBtn.addEventListener('click', () => animate(setTimeout));

// animates a 360 degree rotation of the logo
function animate(fn) {
  let i = 0, done = false;

  logo.style.transform = `rotate(0deg)`;
 
  function cb() {
    i += 1;
    logo.style.transform = `rotate(${i}deg)`;
    if (i > 360) done = true;
    if (!done) fn(cb);
  }

  fn(cb);
}

这是浏览器跳过渲染步骤的另一个示例。该requestAnimationFrame()函数每次在渲染之前都会被调用,因此它会被调用整整 360 次。在这种setTimeout()情况下,浏览器会为多次 (3-4)setTimeout次调用执行一次渲染。因此,这样的动画会运行得更快,并且会跳过很多帧。请看演示。

requestAnimationFrame() 动画不会跳过任何帧。setTimeout 动画会跳过很多帧。

下篇我会写一下如何使用循环事件函数编写高效的代码

有兴趣的同学给个三联不迷路

点赞收藏支持、手留余香、与有荣焉,动动你发财的小手哟,感谢各位大佬能留下您的足迹。

往期热门精彩推荐

面试相关热门推荐

前端万字面经------基础篇

前端万字面积------进阶篇

实战开发相关推荐

前端常用的几种加密方法

探索Web Worker在Web开发中的应用

不懂 seo 优化?一篇文章帮你了解如何去做 seo 优化

【实战篇】微信小程序开发指南和优化实践

前端性能优化实战

聊聊让人头疼的正则表达式

获取文件blob流地址实现下载功能

Vue 虚拟 DOM 搞不懂?这篇文章帮你彻底搞定虚拟 DOM

移动端相关推荐

移动端横竖屏适配与刘海适配

移动端常见问题汇总

聊一聊移动端适配

Git 相关推荐

通俗易懂的 Git 入门

git 实现自动推送

更多精彩详见:个人主页

相关推荐
bysking15 分钟前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
独行soc22 分钟前
#渗透测试#SRC漏洞挖掘#深入挖掘XSS漏洞02之测试流程
web安全·面试·渗透测试·xss·漏洞挖掘·1024程序员节
王哲晓31 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_41134 分钟前
无网络安装ionic和运行
前端·npm
理想不理想v35 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云1 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205871 小时前
web端手机录音
前端
齐 飞1 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod1 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
神仙别闹1 小时前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow