这次,彻底理解 JavaScript 的执行机制

无论你是 JavaScript 的初学者还是专家,无论是为了求职面试还是日常开发工作,我们经常会遇到这样的情况:给出几行代码,我们需要知道它们的输出内容和顺序。由于 JavaScript 是一种单线程语言,我们可以得出以下结论:

JavaScript 按照语句出现的顺序执行。

此时,读者可能会说:我知道 JS 是一行一行执行的,为什么还要特别指出呢?冷静下来;正因为 JS 是一行一行执行的,我们假设所有的 JS 都是这样工作的:

let a = '1';
console.log(a);

let b = '2';
console.log(b);

然而,实际上 JS 是这样的:

setTimeout(function(){
  console.log('定时器开始了')
});
new Promise(function(resolve){
  console.log('即将执行for循环');
  for(var i = 0; i < 10000; i++){
    i == 99 && resolve();
  }
}).then(function(){
  console.log('执行then函数')
});
console.log('代码执行结束');

遵循 JavaScript 按语句顺序执行的概念,我自信地写下了输出:

  1. 定时器开始了。

  2. 即将执行for循环。

  3. 执行then函数。

  4. 代码执行结束。

然而,在 Chrome 中验证时,结果完全错误,瞬间迷惑,难道不是按约定的一行一行执行的吗?

我们需要彻底理解 JavaScript 的执行机制。

关于 JavaScript

JavaScript 是一种单线程语言。尽管在最新的 HTML5 中引入了 Web Worker,但 JavaScript 的单线程核心没有改变。因此,JavaScript 中的所有“多线程”都是使用单线程模拟的,所有的多线程都是欺骗性的!

JavaScript 事件循环

由于 JavaScript 是单线程的,就像只有一个窗口的银行,客户需要一个接一个地排队办理业务。同样,JavaScript 任务也需要一个接一个地执行。如果一个任务花费太长时间,那么下一个任务就必须等待。所以问题来了:如果我们想浏览新闻,但新闻中的高清图片加载缓慢,我们的网页是否必须一直卡住,直到图片完全显示?因此,聪明的程序员将任务分为两类:

  • 同步任务

  • 异步任务

当我们打开一个网站时,网页的渲染过程由一堆同步任务组成,如渲染页面骨架和页面元素。那些消耗资源多、耗时长的任务,如加载图片或音乐文件,则是异步任务。为了简化理解,我们使用思维导图来说明这一点:

b62d18dd98faa608ec2528cdac0c2312.png

如果用文字描述思维导图的内容:

  • 同步任务和异步任务进入不同的执行“场所”,同步任务进入主线程,异步任务进入事件表并注册函数。

  • 当指定任务完成时,事件表会将这个函数移动到事件队列中。

  • 主线程中的任务执行完毕后,会从事件队列中读取相应的函数并在主线程中执行。

  • 上述过程会不断重复,这通常被称为事件循环(Event Loop)。

我们不禁要问,如何知道主线程执行栈是否为空?JavaScript 引擎有一个监视过程,持续检查主线程执行栈是否为空。一旦为空,它就会去事件队列中检查是否有等待调用的函数。

经过以上描述,一段代码可能会更直观:

let data = [];
$.ajax({
    url: 'www.javascript.com',
    data: data,
    success: () => {
        console.log('发送成功');
    }
})
console.log('代码执行结束');

上面是一段简单的 ajax 请求代码:

  • ajax 进入事件表并注册回调函数 success。

  • 执行 console.log('代码执行结束')。

  • ajax 事件完成,回调函数 success 进入事件队列。

  • 主线程从事件队列中读取并执行回调函数 success。

通过以上的文字和代码,相信你对 JavaScript 的执行顺序有了初步的了解。接下来,让我们研究一个高级话题:setTimeout

对 setTimeout 的爱恨情仇

众所周知,setTimeout 无需过多介绍。我们对它的第一印象是它可以在延迟之后异步执行。我们经常使用它来实现 3 秒延迟执行:

setTimeout(() => {
    task();
}, 3000)
console.log('执行 console');

随着 setTimeout 的使用逐渐增多,问题也随之而来。有时,即使在代码中指定了 3 秒的延迟,函数也会在 56 秒后执行。这可能是什么原因造成的呢?

我们先看一个例子:

setTimeout(() => {
    task();
}, 3000)
console.log('执行 console');

根据我们之前的结论,setTimeout 是异步的,所以同步任务 console.log 应该先执行。因此,我们的结论是:

  1. 执行 console

  2. task()

为了验证,结果是正确的!然后让我们对之前的代码做一些修改:

setTimeout(() => {
    task();
}, 3000)

sleep(10000000)

乍一看,这似乎类似,但当我们在 Chrome 中执行这段代码时,发现 console 的执行时间远远超过 3 秒。为什么现在需要这么长时间呢?

此时,我们需要重新定义 setTimeout。让我们来讨论上面代码的执行过程:

  1. task() 进入事件表并注册,计时开始。

  2. 执行非常缓慢的 sleep 函数,计时继续。

  3. 3 秒钟过去,计时事件 timeout 完成。task() 进入事件队列。但是,sleep 太慢,还没有执行完毕;所以我们必须等待。

  4. 最后,sleep 执行完毕。task() 终于从事件队列移动到主线程执行。

经过上述过程,我们了解到 setTimeout 函数会在指定时间后将任务(在这个例子中是 task())添加到事件队列中。由于任务在单线程环境中一个接一个地执行,如果前面的任务执行时间过长,执行时间将显著超过 3 秒。

我们经常遇到类似 setTimeout(fn, 0) 的代码。0 秒后执行意味着什么?它能立即执行吗?

答案是否定的。setTimeout(fn, 0) 的意思是指定某个任务在主线程最早的空闲时间执行,不需要等待任何额外的秒数,一旦所有同步任务在栈中完成并且栈变为空。例如:

// 代码 1
console.log('先执行这里');
setTimeout(() => {
    console.log('执行了')
}, 0);

// 代码 2
console.log('先执行这里');
setTimeout(() => {
    console.log('执行了')
}, 3000);

代码 1 的输出结果是:

  1. 先执行这里

  2. 执行了

代码 2 的输出结果是:

  1. 先执行这里

  2. ... 3 秒后

  3. 执行了

关于 setTimeout 需要注意的是,即使主线程空闲,0 毫秒也无法实现。根据 HTML 标准,最小值为 4 毫秒。感兴趣的同学可以自行探索。

双胞胎兄弟 setInterval

谈到 setTimeout,我们不能错过它的双胞胎兄弟 setInterval。它们很相似,只不过后者是循环执行的。从执行顺序来看,setInterval 会在每个指定的间隔时间将注册的函数放入事件队列。如果前一个任务花费太长时间,它也需要等待。

唯一需要注意的是,对于 setInterval(fn, ms),我们已经知道 fn 不会每 ms 秒执行一次,而是在每 ms 秒将一个新的 fn 实例放入事件队列。如果 setInterval 的回调函数(fn)花费的时间超过了延迟时间(ms),那么将不会有明显的时间间隔。请仔细思考这句话。

Promise 和 process.nextTick(callback)

我们已经研究了传统的定时器,接下来,我们将探索 Promiseprocess.nextTick(callback) 的表现。

Promise 的定义和功能在本文中不会详细展开。而 process.nextTick(callback) 类似于 Node.js 中的 “setTimeout”,在事件循环的下一轮调用回调函数。

切入正题,除了同步任务和异步任务的广义定义外,我们还有更精细的任务定义:

  • 宏任务(macro-task):包括整体代码、setTimeout、setInterval

  • 微任务(micro-task):Promise、process.nextTick

不同类型的任务将进入相应的事件队列;例如,setTimeoutsetInterval 将进入同一个事件队列。

事件循环中的事件顺序决定了 JavaScript 代码的执行顺序。在进入整体代码(宏任务)后,它开始其第一次循环。然后,它执行所有的微任务。接下来,它再次从宏任务开始,直到一个任务队列完成,再次执行所有的微任务。听起来有点复杂;让我们用本文前面的一个代码片段来说明:

setTimeout(function() {
    console.log('setTimeout');
})

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');
  1. 这段代码作为宏任务进入主线程。

  2. 遇到 setTimeout,它的回调函数被注册并分派到宏任务事件队列中。

  3. 接下来,遇到 Promise,new Promise 立即执行,并将 then 函数分派到微任务事件队列中。

  4. 遇到 console.log(),立即执行。

  5. 在作为第一个宏任务执行整体代码后,我们看看有哪些微任务。我们发现 then 在微任务事件队列中,并执行它。

  6. 事件循环的第一轮结束。让我们从宏任务事件队列开始第二轮循环。我们发现这个队列中对应于 setTimeout 的回调函数立即执行。

  7. 结束

事件循环、宏任务和微任务之间的关系如图所示:

954f7e54db4d4dc2ecca607a90d8a8c0.png

我们分析一段更复杂的代码,看看您是否理解了 JavaScript 的执行机制:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

事件循环第一轮过程分析如下:

  • 整体代码作为第一个宏任务进入主线程,遇到 console.log 并输出 1。

  • 遇到 setTimeout,它的回调函数被分派到宏任务事件队列中,我们暂时称之为 setTimeout1。

  • 遇到 process.nextTick(),它的回调函数被分派到微任务事件队列中,我们称之为 process1。

  • 遇到 Promise,new Promise 直接执行并输出 7,then 方法分派到微任务事件队列中,我们称之为 then1。

  • 再次遇到 setTimeout,它的回调函数被分派到宏任务事件队列中,我们称之为 setTimeout2。

20ef3a6225f5630db3b4028677685774.png

  • 在事件循环宏任务第一轮结束时,输出 1 和 7。

  • 我们发现两个微任务:process1 和 then1。

  • 执行 process1 输出 6。

  • 执行 then1 输出 8。

第一轮事件循环正式结束,结果输出为 1, 7, 6, 8。第二轮事件循环从 setTimeout1 宏任务开始:

  • 首先,输出 2。接下来,遇到 process.nextTick(),将其分派到微任务事件队列中,标记为 process2。new Promise 立即执行并输出 4,然后分派到微任务事件队列中,标记为 then2。

bf786c9776160ce87cfc84b964422e15.png

  • 在第二轮事件循环宏任务结束后,我们发现有两个微任务,process2 和 then2,可以执行。

  • 输出 3。

  • 输出 5。

  • 第二轮事件循环结束,输出为 2, 4, 3, 5。

  • 第三轮事件循环开始,此时只有 setTimeout2 剩下等待执行。

  • 直接输出 9。

  • 分派 process.nextTick() 到微任务事件队列,标记为 process3。

  • new Promise 直接执行并输出 11。

  • 分派 then 到微任务事件队列,标记为 then3。

9d788312f3e364b1d458e06d1ef96097.png

  • 第三轮事件循环宏任务执行完成,执行两个微任务 process3 和 then3。

  • 输出 10。

  • 输出 12。

  • 第三轮事件循环结束,输出为 9, 11, 10, 12。

整个代码段经过了三轮事件循环,完整输出为 1, 7, 6, 8, 2, 4, 3, 5, 9, 11, 10, 12。

在 Node 环境中的事件监听依赖于 libuv,与前端环境不完全相同,输出顺序可能会有差异。

总结

JavaScript 的异步性:从一开始,我们就说过 JavaScript 是单线程语言。无论使用什么新框架或语法糖来实现所谓的异步性,都是通过同步方法模拟的。牢牢把握单线程这一点非常重要。

事件循环:事件循环是 JavaScript 实现异步操作的方法,也是其执行机制。

JavaScript 的执行与运行:执行和运行有很大区别。JavaScript 的执行方式在不同环境中有所不同,如 Node.js、浏览器、Ringo 等。然而,运行大多指 JavaScript 解析引擎,保持一致。

setImmediate:有许多类型的微任务和宏任务,如 setImmediate 等,它们的执行有共同点。感兴趣的同学可以自行探索。

最后但同样重要的是:JavaScript 是单线程语言,事件循环是其执行机制。 牢牢掌握这两个基本点,认真学习 JavaScript,很快实现成为优秀前端开发者的伟大梦想!

最后:

vue2与vue3技巧合集

VueUse源码解读

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/732664.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

南昌代理记账服务,打造专业财务管理团队

随着企业的发展和规模不断扩大&#xff0c;财务管理也变得越来越重要&#xff0c;而代理记账作为一种专业的财务管理服务&#xff0c;正逐步成为中小企业发展的必要条件之一&#xff0c;南昌作为江西省的省会城市&#xff0c;拥有一批优秀的会计服务机构&#xff0c;为各类企业…

基于Java学生选课管理系统设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f; 感兴趣的可以先收藏起来&#xff0c;…

【启明智显产品分享】工业级HMI芯片——Model3,不止是速度,USB\CAN\8路串口

一、引言 Model3作为一款工业级HMI芯片&#xff0c;其性能卓越且功能全面。本文将从多个角度深入介绍Model3芯片&#xff0c;以展示其不仅仅是速度的代表。 二、Model3核心特性介绍 Model3工业级跨界MCU是一款国产自主的基于RISC-V架构的高性能芯片&#xff0c;内置平头哥E…

你只是重新发现了一些东西

指北君关于另外一条思维路径的发现。 "自以为是"的顿悟时刻 有很多时候&#xff0c;我会"自以为是"的发现/发明一些东西。这种"自以为是"的时刻通常还带有一些骄傲自豪的情绪。这种感觉特别像古希腊博学家阿基米德 在苦思冥想如何测量不规则物体…

windows使用curl命令出现乱码的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

deepin 加入甲辰计划,共建 RISC-V 繁荣生态

内容来源&#xff1a;deepin&#xff08;深度&#xff09;社区 今日&#xff0c;deepin(深度)社区宣布正式加入甲辰计划&#xff0c;致力于在下一个丙辰年&#xff08;2036龙年&#xff09;之前&#xff0c;基于RISC-V实现从数据中心到桌面办公、从移动穿戴到智能物联网全信息产…

通俗解释resultType和resultMap的区别

【 1 对于单表而言&#xff1a; 注&#xff1a;以下都是摘抄过来的&#xff0c;做了让自己更能理解的版本 如果数据库返回结果的列名和要封装的实体的属性名完全一致的话用 resultType 属性 如果数据库返回结果的列名&#xff08;起了别名&#xff09;和要封装的实体的属性名…

Android使用MPAndroidChart 绘制折线图

效果图&#xff1a; 1.导入依赖 1.1在项目根目录下的build.gradle文件中添加代码&#xff08;注意不是app下的build.gradle&#xff09;&#xff1a; maven { url https://jitpack.io } 1.2在app下的build.gradle中的依赖下添加&#xff1a; implementation com.github.PhilJa…

STM32单片机USART串口收发数据包

文章目录 1. 串口通信 1.1 串口初始化 1.2 库函数 2. 串口收发HEX数据包 2.1 Serial.c 2.2 Serial.h 2.3 main.c 3. 串口收发文本数据包 3.1 Serial.c 3.2 Serial.h 3.3 main.c​​​​​​​ 1. 串口通信 对于串口通信的详细​​​​​​​解析可以看下面这篇文章…

生产实习Day7 ---- Hive介绍

文章目录 概要整体架构流程Hive数据库操作建表语法表分类 概要 Hive是基于Hadoop的数据仓库工具。可以用于存储在Hadoop集群中的HDFS文件数据集进行数据整理、特殊查询和分析处理。Hive提供了类似于关系型数据库SQL语言的HiveQL工具&#xff0c;通过HQL&#xff08;HiveQL&…

用VPS部署聊天机器人有哪些优势?

VPS足以帮助您将人工智能 (AI) 的功能无缝融入聊天机器人并增强客户支持。聊天机器人已迅速成为改善用户体验的首选解决方案&#xff0c;因为它们全天候在线且可编程回答各种问题。 聊天机器人在客户支持方面的作用不容置疑。但所有出色的解决方案都需要出色的网络托管。 VPS…

从零到发布:npm插件包终极指南

在JavaScript和Node.js的生态系统中&#xff0c;npm&#xff08;Node Package Manager&#xff09;是最重要的包管理工具之一。通过npm&#xff0c;开发者可以共享代码、复用他人的工作成果以及协作开发。本指南将详细介绍如何通过npm发布自己的插件包&#xff0c;以便其他开发…

力扣372. 超级次方

Problem: 372. 超级次方 文章目录 题目描述思路复杂度Code 题目描述 思路 1.处理数组指数&#xff1a;如下图可以将其转换为一个递归的操作 2.处理 mod 运算:对于模运算我们有公式&#xff1a; ( a b ) % k ( a % k ) ( b % k ) % k (a \times b) \% k (a \% k) \times (b…

如何利用数据仓库进行业务分析:一名大数据工程师的视角

在大数据时代&#xff0c;数据的有效利用对企业的成功至关重要。 本文将基于上面的流程图&#xff0c;详细介绍如何利用数据仓库进行业务分析&#xff0c;并提供实际的例子和代码演示&#xff0c;以帮助读者更好地理解和应用相关技术。 数据仓库的基本流程 上图展示了一个典…

PointCloudLib 点云边缘点提取 C++版本

0.实现效果 1.算法原理 PCL(Point Cloud Library)中获取点云边界的算法主要基于点云数据的几何特征和法向量信息。以下是对该算法的详细解释,按照清晰的格式进行归纳: 算法概述 PCL中的点云边界提取算法主要用于从3D点云数据中识别并提取出位于物体边界上的点。这些边界…

五种主流数据库连接池的比较和分析(从零入门)

数据库连接池&#xff08;Database Connection Pool&#xff09;是现代应用程序中至关重要的组件。它通过管理一组数据库连接的复用&#xff0c;极大地提升了应用程序的性能和效率。在没有连接池的情况下&#xff0c;每个数据库请求都需要打开和关闭数据库连接&#xff0c;这不…

【C++】关于代码编译自动更新版本的问题

在写代码的时候&#xff0c;总是需要添加一个版本号&#xff0c;用于后续的版本管理 我常遇到的一个问题是&#xff0c;开发过程中&#xff0c;不一定会记得这件事情&#xff0c;导致有时候会出现同样的版本 于是希望有一个方式&#xff0c;能在编译代码的时候自动生成一个版…

【机器学习 复习】第11章 神经网络与深度学习(重中之重)

一、概念 1.神经元模型 &#xff08;1&#xff09;神经网络的基本组成单位 &#xff08;2&#xff09;生物上&#xff0c;每个神经元通过树突接受来自其他被激活神经元的信息&#xff0c;通过轴突释放出来的化学递质改变当前神经元内的电位。当神经元内的电位累计到一个水平时…

从同—视角理解扩散模型(Understanding Diffusion Models A Unified Perspective)

从同—视角理解扩散模型 Understanding Diffusion Models A Unified Perspective【全公式推导】【免费视频讲解】 B站视频讲解 视频的论文笔记 从同一视角理解扩散模型【视频讲解笔记】 配合视频讲解的同步笔记。 整个系列完整的论文笔记内容如下&#xff0c;仅为了不用—一回复…

在Linux下使用CMake加载自定义路径第三方库的指南

CMake是一个强大的跨平台构建系统&#xff0c;广泛应用于C项目中。它不仅能够处理标准的构建过程&#xff0c;还可以灵活地集成各种第三方库&#xff0c;包括自定义路径的库、已编译的共享库&#xff08;.so 文件&#xff09;&#xff0c;以及仅包含头文件的库&#xff08;如Ei…