js代码03
题目
好的,我们进入 JavaScript 的另一个核心领域:异步编程。
在之前的练习中,我们写的代码都是同步 (Synchronous) 的,也就是说,代码从上到下一行一行地执行,上一行没执行完,下一行就绝不会开始。
但现实世界是异步 (Asynchronous) 的。比如,我们向服务器请求数据,这个过程可能需要几百毫-秒甚至几秒钟。如果我们的程序傻傻地“同步”等待,那么在数据返回之前,整个页面(比如网页上的按钮、动画)都会被卡住,无法响应用户操作。这就是所谓的“阻塞 (Blocking)”。
为了解决这个问题,JavaScript 采用了异步编程模型。
练习 03: 异步的初步体验 - 回调与 setTimeout
这个练习将带你初次体验异步操作,并了解早期处理异步的一种方式:回调函数 (Callback Functions)。
🎯 学习目标:
- 理解同步与异步代码执行流程的区别。
- 学会使用
setTimeout
来模拟一个耗时操作。 - 理解并使用回调函数来处理异步操作的结果。
- (可选) 初步感受“回调地狱 (Callback Hell)”是什么样的。
背景知识:
setTimeout(callback, delay)
: 这是浏览器和 Node.js 都提供的一个全局函数。它接受两个参数:callback
: 一个函数,这个函数将会在指定的延迟时间之后被执行。delay
: 一个以毫秒为单位的数字,表示要延迟多久才执行callback
函数。
- 事件循环 (Event Loop): 这是 JavaScript 异步机制的核心。你现在只需要知道:当你调用
setTimeout
时,JavaScript 引擎并不会停在那里等待,而是会把这个“任务”(即你的回调函数)交给它的一个“助手”(比如浏览器内核)。然后,JS 引擎会继续执行后面的同步代码。当指定的延迟时间到了,助手就会把这个任务放回一个“任务队列”中,等待 JS 引擎空闲时再去执行它。
🛠️ 任务:
我们要模拟一个“获取用户数据”的场景。这个操作通常是异步的,因为它需要时间去网络上请求。
- 创建一个名为
fetchUserData
的函数,它接受两个参数:userId
(一个数字,代表用户ID)。callback
(一个函数,当数据“获取”成功后,我们用它来处理数据)。
- 在
fetchUserData
函数内部,使用setTimeout
来模拟一个 2 秒钟的网络延迟。 - 在 2 秒钟后,
setTimeout
的回调函数应该:- 创建一个模拟的用户数据对象,例如
{ id: userId, name: 'John Doe', email: 'john.doe@example.com' }
。 - 调用我们作为参数传入的
callback
函数,并将这个模拟的用户数据对象作为参数传给它。
- 创建一个模拟的用户数据对象,例如
- 在主代码中,调用
fetchUserData
函数,并提供一个回调函数来打印获取到的用户信息。 - 在调用
fetchUserData
之后,立即打印一条消息,如 “正在获取用户数据,请稍候…”,以观察同步和异步代码的执行顺序。
📋 初始代码:
创建新文件 03-callbacks.js
,并复制以下代码。
console.log("程序开始执行...");/*** 模拟从服务器获取用户数据。* @param {number} userId - 要获取的用户的ID。* @param {function(object): void} callback - 数据获取成功后要执行的回调函数。*/
function fetchUserData(userId, callback) {// --- 在这里编写你的代码 ---// 1. 使用 setTimeout 模拟网络延迟。// 2. 在 setTimeout 的回调中,创建一个假的用户对象。// 3. 调用传入的 callback 函数,并把用户对象作为参数传进去。}// 调用 fetchUserData,并传入一个回调函数来处理结果
fetchUserData(123, (user) => {console.log("成功获取到用户数据!");console.log(`用户姓名: ${user.name}`);console.log(`用户邮箱: ${user.email}`);
});console.log("程序继续执行... (这行代码会先于用户数据被打印)");
✅ 预期输出:
运行代码后,你应该会看到这样的输出顺序:
程序开始执行...
(立刻打印)程序继续执行... (这行代码会先于用户数据被打印)
(立刻打印)- (等待大约 2 秒钟)
成功获取到用户数据!
(2秒后打印)用户姓名: John Doe
(2秒后打印)用户邮箱: john.doe@example.com
(2秒后打印)
这个输出顺序完美地展示了异步编程的非阻塞特性。程序不会傻等 fetchUserData
完成,而是会继续执行后面的代码。
这个练习是理解后续 Promise、async/await 的基石。开始吧!
答案
当然,我们一起来看这个实现,并深入分析代码的执行流程,这对于理解异步至关重要。
参考实现 (03-callbacks.js
)
console.log("程序开始执行...");/*** 模拟从服务器获取用户数据。* @param {number} userId - 要获取的用户的ID。* @param {function(object): void} callback - 数据获取成功后要执行的回调函数。*/
function fetchUserData(userId, callback) {// 1. 使用 setTimeout 模拟一个 2000 毫秒(2秒)的网络延迟。setTimeout(() => {// 这部分代码将会在 2 秒后执行...console.log("...网络请求耗时2秒,现在返回数据...");// 2. 创建一个假的用户对象。const user = {id: userId,name: 'John Doe',email: 'john.doe@example.com'};// 3. 调用我们当初传入的 callback 函数,并把用户对象作为参数传进去。callback(user);}, 2000); // 延迟 2000 毫秒
}// 调用 fetchUserData,并传入一个回调函数来处理结果
fetchUserData(123, (user) => {console.log("成功获取到用户数据!");console.log(`用户姓名: ${user.name}`);console.log(`用户邮箱: ${user.email}`);
});console.log("程序继续执行... (这行代码会先于用户数据被打印)");
代码执行流程全解析
这正是异步编程最有趣也最反直觉的地方。让我们一步步追踪 JavaScript 引擎的“思考过程”:
-
第 1 步: 引擎看到
console.log("程序开始执行...");
。这是一个同步任务,立刻执行。- 控制台输出:
程序开始执行...
- 控制台输出:
-
第 2 步: 引擎看到
fetchUserData(123, (user) => { ... });
这个函数调用。它立刻进入fetchUserData
函数内部。userId
参数被设为123
。callback
参数被设为我们定义的那个箭头函数(user) => { ... }
。
-
第 3 步 (最关键的一步): 在
fetchUserData
函数内部,引擎遇到了setTimeout(...)
。setTimeout
本质上是在对 JavaScript 引擎说:“嘿,这里有一个任务(() => { ... }
这个函数),请你帮我设置一个 2 秒的定时器。时间到了之后,再把这个任务放到待办列表里去执行。现在你不用管它,请继续往下走!”- 于是,JavaScript 引擎设置好了定时器,然后就立刻离开了
fetchUserData
函数,继续执行后面的代码。它不会在这里傻等 2 秒钟。
-
第 4 步: 引擎执行
fetchUserData
调用之后的下一行代码:console.log("程序继续执行...");
。这也是一个同步任务,立刻执行。- 控制台输出:
程序继续执行... (这行代码会先于用户数据被打印)
- 控制台输出:
-
第 5 步: 所有同步代码都执行完了。现在 JavaScript 引擎处于“空闲”状态,但它设置的那个 2 秒的定时器还在后台默默计时。
-
第 6 步 (2秒后): 定时器时间到!后台的“助手”把
setTimeout
的回调函数() => { ... }
放进了任务队列。JavaScript 的事件循环 (Event Loop) 发现自己空闲,并且队列里有新任务,于是就把它拿出来执行。 -
第 7 步: 现在,
setTimeout
内部的代码终于被执行了。console.log("...网络请求耗时2秒...");
被执行。user
对象被创建。callback(user)
被调用。这里的callback
就是我们在第 2 步传入的那个函数。于是,user
对象被传递给了它。- 控制台输出:
...网络请求耗时2秒,现在返回数据... 成功获取到用户数据! 用户姓名: John Doe 用户邮箱: john.doe@example.com
“回调地狱”的预兆
回调函数是解决异步问题的有效方式,但如果存在多个互相依赖的异步操作,事情就会变得棘手。
想象一下,你需要:
- 先获取用户信息。
- 然后根据用户信息获取他的文章列表。
- 再然后根据第一篇文章获取其所有评论。
使用回调函数,代码会变成这样:
fetchUserData(123, (user) => {console.log("拿到了用户信息:", user);fetchUserPosts(user.id, (posts) => {console.log("拿到了用户文章:", posts);fetchPostComments(posts[0].id, (comments) => {console.log("拿到了文章评论:", comments);// 如果还有下一步,就要继续往里嵌套...// ...});});
});
这种不断向右缩进的嵌套结构,就是臭名昭著的 “回调地狱” (Callback Hell)。它极难阅读、维护和进行错误处理。
为了解决“回调地狱”这个问题,JavaScript 引入了更优雅、更强大的异步处理方案:Promise。这将是我们下一个练习的核心。
你已经成功理解了异步和回调,这是非常重要的一步!准备好后,我们就去挑战 Promise,彻底告别回调地狱。