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 是单线程的,定时器的回调函数会在主线程空闲时执行,如果主线程被长时间占用,定时器的执行时间可能会有偏差。
相关推荐
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端