JavaScript 异步操作入门指南与基础实践

在 JavaScript 的世界里,异步操作是一个强大且不可或缺的特性,掌握异步操作将让你的编程技能更上一层楼。本文将深入探讨 JavaScript 异步操作的核心概念、常见应用场景以及不同的实现路径,帮助你全面理解和运用这一重要特性。

一、同步与异步:核心概念

1.1 同步操作

同步操作如同多米诺骨牌,前一块骨牌倒下触发后,下一块骨牌才会紧接着倒下。在 JavaScript 里,代码也遵循这样的同步顺序,一个操作完成后,下一个操作才会启动。例如:

javascript 复制代码
console.log('开始同步任务');
let sum = 0;
for (let i = 0; i < 1000000; i++) {
    sum += i;
}
console.log('同步任务计算结果:', sum);
console.log('同步任务结束');

示例代码中,console.log('开始同步任务') 执行完毕后,才会进入 for 循环进行计算,for 循环结束得到计算结果,才会继续执行后续的 console.log 语句输出结果和结束信息。整个过程是线性的,按部就班,不会跳过任何一个步骤。如果某个操作耗时过长,比如 for 循环中计算量非常大,后续的代码就会被阻塞,无法及时响应其他操作。

1.2 异步操作

异步操作类似在线文档多人协作编辑。假设有一个在线文档,有用户 A、用户 B 和用户 C 同时在编辑它。用户 A 在文档开头部分修改文字,用户 B 在中间插入图片,用户 C 在结尾处添加表格,他们的操作同时进行,彼此互不干扰。

在 JavaScript 里,当发起一个异步操作,就如同用户 A 开始在文档开头修改文字这个动作。程序不会停下手中其他的 "事情",专门等待用户 A 修改完,而是继续执行后续代码。直到用户 A 完成了修改(也就是异步操作完成),在线文档会更新,把用户 A 修改后的内容展示出来。例如使用setTimeout函数:

javascript 复制代码
console.log('开始异步任务');
let sum = 0;
setTimeout(() => {
    for (let i = 0; i < 1000000; i++) {
        sum += i;
    }
    console.log('异步任务计算结果:', sum);
    console.log('异步任务结束');
}, 0);
console.log('在异步任务执行期间,程序继续执行其他代码');
1.2.1 代码初始状态与 "发起异步操作"
javascript 复制代码
console.log('开始异步任务');
let sum = 0;

在这个阶段,程序输出 开始异步任务 信息,同时声明并初始化变量 sum 为 0。这就好比在线文档系统开始运作,所有编辑人员(对应代码里的任务)都准备就绪。此时,我们发起了一个异步操作,就如同用户 A 准备开始在文档开头修改文字。

1.2.2. 调用 setTimeout 函数
javascript 复制代码
setTimeout(() => {
    for (let i = 0; i < 1000000; i++) {
        sum += i;
    }
    console.log('异步任务计算结果:', sum);
    console.log('异步任务结束');
}, 0);

setTimeout 函数的作用是设定一个定时器,当达到指定的延迟时间后执行回调函数。这里延迟时间为 0 毫秒,意味着回调函数会在当前同步代码执行完毕后尽快执行。

这就类似用户 A 已经开始在文档开头修改文字这个动作,但程序不会停下手中其他的 "事情" 专门等待用户 A 修改完。也就是说,JavaScript 不会等待 setTimeout 的回调函数执行,而是会继续执行后续的同步代码。

1.2.3. 继续执行后续同步代码
javascript 复制代码
console.log('在异步任务执行期间,程序继续执行其他代码');

程序执行这行代码,输出 在异步任务执行期间,程序继续执行其他代码。这就如同在用户 A 编辑文档的同时,用户 B 可以在文档中间插入图片,用户 C 可以在文档结尾添加表格,各个操作互不干扰,程序继续进行其他任务。

1.2.4. 执行 setTimeout 的回调函数

当所有同步代码执行完毕后,事件循环会检查任务队列,发现 setTimeout 的回调函数,然后执行它。

javascript 复制代码
for (let i = 0; i < 1000000; i++) {
    sum += i;
}
console.log('异步任务计算结果:', sum);
console.log('异步任务结束');

在回调函数中,通过 for 循环进行累加计算,计算完成后输出计算结果和异步任务结束的信息。这就好比用户 A 完成了在文档开头的文字修改,在线文档会更新,把用户 A 修改后的内容展示出来,对应代码里就是完成计算并输出结果。

二、常见应用场景

2.1 网络请求

在现代 Web 开发中,网络请求无处不在。无论是从服务器获取数据以填充页面内容,如从数据库获取文章列表展示在博客首页,还是向服务器提交用户表单数据,比如用户注册信息,都涉及到网络请求。

由于网络传输存在延迟,若使用同步请求,在等待数据返回的过程中,页面会处于假死状态,用户无法进行任何操作,体验极差。而异步网络请求(如fetch API)则允许浏览器在发送请求后继续响应用户操作,如滚动页面、点击其他链接等,当数据返回时,再通过回调函数或 Promise 来处理数据,更新页面。

javascript 复制代码
fetch('https://randomuser.me/api/') 
  .then(response => response.json())
  .then(data => {
        console.log('从服务器获取的数据:', data);
        // 这里简单示例如何根据数据更新页面,假设页面有一个id为userInfo的元素用来显示用户信息
        const userInfoElement = document.getElementById('userInfo');
        if (userInfoElement) {
            userInfoElement.innerHTML = `
                <p>姓名: ${data.results[0].name.first} ${data.results[0].name.last}</p>
                <p>邮箱: ${data.results[0].email}</p>
                <p>地址: ${data.results[0].location.street.number} ${data.results[0].location.street.name}, ${data.results[0].location.city}, ${data.results[0].location.state}, ${data.results[0].location.country}</p>
            `;
        }
    })
  .catch(error => console.error('网络请求出错:', error));
第一步:发起异步请求
  • 当代码执行到fetch('https://randomuser.me/api/')时,fetch会立即向https://randomuser.me/api/这个网址发送一个 HTTP GET 请求,去获取数据。
  • fetch不会等数据获取回来才继续往下执行,而是马上返回一个 Promise 对象。这个 Promise 对象就像是一个 "未来会有结果" 的信号。
  • 此时,JavaScript 引擎不会停下来等待数据,而是继续执行后续的.then链式调用代码。在网页环境中,这意味着当用户发起这个请求时,他们仍然可以自由地点击页面上的按钮、滚动页面,因为主线程没有被等待数据这件事卡住。
第二步:处理响应的异步性
  • 如果fetch返回的 Promise 对象表明请求成功了,代码就会进入.then(response => response.json())这个回调函数。
  • 这里的response包含了服务器返回的所有信息。response.json()是一个将服务器返回的响应内容解析成 JSON 格式的异步操作,它会返回一个新的 Promise 对象。
  • 当调用response.json()时,JavaScript 引擎不会等待解析完成,而是直接带着这个新的 Promise 对象继续执行后续代码(如果有后续代码的话)。只有当 JSON 解析真正完成了,与这个新 Promise 对象相关联的下一个.then回调函数才会被触发。
第三步:链式调用与异步顺序控制
  • response.json()返回的 Promise 对象成功解决,也就是 JSON 解析完成后,.then(data => {... })这个回调函数就会开始执行。这里的data就是解析好的 JSON 数据。
  • 通过链式调用.then方法,我们可以按照特定的顺序处理这些异步操作的结果。虽然在每个.then回调函数内部,代码是按顺序一行一行执行的,比如先打印获取到的数据,再尝试获取页面上指定id的元素并更新其内容。
  • 但从整体来看,网络请求和 JSON 解析这些操作都是异步的,不会阻塞主线程。这就保证了在等待网络请求完成和数据解析的过程中,网页仍然可以与用户进行交互,用户能够正常地进行各种操作。
第四步:错误处理的异步性
  • .catch(error => console.error('网络请求出错:', error))的作用是捕获整个异步操作过程中出现的错误。
  • 这些错误可能包括fetch请求失败,比如网络连接不上,或者服务器没有响应;也可能是response.json()解析失败,比如服务器返回的数据格式不符合 JSON 规范。
  • 无论错误出现在异步操作的哪个步骤,.catch回调函数都会在合适的时候被调用,用来处理这些错误信息。并且,它不会影响页面上其他正在进行的异步操作流程。
  • 例如,如果fetch请求失败了,.catch会捕获这个错误并在控制台打印出错误信息,而页面上其他的动画效果、其他网络请求等都可以继续正常运行。

请求成功的结果

2.2 文件操作

在 Node.js 环境中,文件操作也是一个常见的场景。文件的读写操作可能会比较耗时,如果使用同步方式进行文件操作,会阻塞 Node.js 的事件循环,影响程序的性能。因此,Node.js 提供了异步的文件操作 API。例如:

javascript 复制代码
const fs = require('fs');
console.log('开始读取文件');
fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
        console.error('读取文件出错:', err);
    } else {
        console.log('文件内容:', data);
    }
});
console.log('在等待文件读取结果时,程序继续执行');

在这个例子中,fs.readFile 是一个异步的文件读取操作。程序在发起读取请求后,会继续执行后续的 console.log 语句,而不会等待文件读取完成。当文件读取完成后,会通过回调函数处理结果。

2.3 定时器操作

JavaScript 提供了 setTimeoutsetInterval 两个函数用于定时器操作。这两个函数都是异步的,它们允许你在指定的时间后执行某个函数,或者每隔一段时间重复执行某个函数,在很多场景中,如动画效果、轮询数据获取等方面都有着广泛的应用。

1. setTimeout 函数

setTimeout 函数用于在指定的毫秒数后执行一次指定的函数。它接受两个参数:第一个参数是要执行的函数(可以是匿名函数或已定义的函数),第二个参数是延迟的毫秒数。

例如:

javascript 复制代码
console.log('设置定时器');
setTimeout(() => {
    console.log('定时器触发,2秒过去了');
}, 2000);
console.log('在等待定时器触发时,程序继续执行');

在这个例子中,setTimeout 函数设置了一个 2 秒后执行的定时器。程序在设置好定时器后,会继续执行后续的 console.log 语句,而不会等待 2 秒。2 秒后,定时器的回调函数会被执行。

2. setInterval 函数

setInterval 函数用于按照指定的时间间隔(以毫秒为单位)重复执行某个函数。它同样接受两个参数:第一个参数是要执行的函数(可以是匿名函数或已定义的函数),第二个参数是时间间隔的毫秒数。

以下是一个示例:

javascript 复制代码
console.log('设置重复定时器');
const intervalId = setInterval(() => {
    console.log('定时器再次触发,又过了1秒');
}, 1000);

// 5秒后停止定时器
setTimeout(() => {
    clearInterval(intervalId);
    console.log('定时器已停止');
}, 5000);

在上述代码中:

  • 首先通过 setInterval 设置了一个每隔 1 秒执行一次的定时器,每次执行时会在控制台打印相应的信息。
  • 然后使用 setTimeout 函数设置了一个 5 秒后的定时器,当这个 5 秒的定时器触发时,会调用 clearInterval 函数,并传入 setInterval 返回的 intervalId(用于标识定时器的唯一标识符),从而停止之前设置的重复定时器。
  • 在使用 setInterval 时,要确保在适当的时候停止定时器,避免资源浪费或出现意外的重复执行情况。同时,由于 JavaScript 是单线程的,定时器的回调函数会在主线程空闲时执行,如果主线程被长时间占用,定时器的执行时间可能会有偏差。
相关推荐
风中飘爻36 分钟前
JavaScript:BOM编程
开发语言·javascript·ecmascript
恋猫de小郭38 分钟前
Android Studio Cloud 正式上线,不只是 Android,随时随地改 bug
android·前端·flutter
清岚_lxn5 小时前
原生SSE实现AI智能问答+Vue3前端打字机流效果
前端·javascript·人工智能·vue·ai问答
ZoeLandia6 小时前
Element UI 设置 el-table-column 宽度 width 为百分比无效
前端·ui·element-ui
橘子味的冰淇淋~6 小时前
解决 vite.config.ts 引入scss 预处理报错
前端·vue·scss
萌萌哒草头将军7 小时前
💎这么做,cursor 生成的代码更懂你!💎
javascript·visual studio code·cursor
小小小小宇8 小时前
V8 引擎垃圾回收机制详解
前端
lauo8 小时前
智体知识库:ai-docs对分布式智体编程语言Poplang和javascript的语法的比较(知识库问答)
开发语言·前端·javascript·分布式·机器人·开源
拉不动的猪8 小时前
设计模式之------单例模式
前端·javascript·面试
一袋米扛几楼989 小时前
【React框架】什么是 Vite?如何使用vite自动生成react的目录?
前端·react.js·前端框架