JavaScript 是一门单线程语言,这意味着在同一时间内,它只能做一件事情。如果遇到一个耗时很长的操作(比如网络请求、文件读写),程序就必须等待这个操作完成后才能继续执行下一步,这会导致页面“假死”,用户体验极差。
为了解决这个问题,JavaScript 引入了异步编程模型。
代码按照从上到下的顺序依次执行,后一个任务必须等待前一个任务执行完毕才能开始。
console.log("任务 1:开始");
// 这是一个耗时的同步操作// 在真实场景中,可能是复杂计算或同步的 API 调用for (let i = 0; i < 1e9; i++) { // 模拟耗时}
console.log("任务 2:一个耗时的操作完成");console.log("任务 3:结束");
// 输出顺序:// 任务 1:开始// (等待几秒钟)// 任务 2:一个耗时的操作完成// 任务 3:结束在同步模式下,“任务 3” 必须等待那个耗时的 for 循环结束后才能被打印。
当遇到一个耗时操作时,程序不会等待它完成,而是立即开始执行下一个任务。耗时操作会在未来的某个时刻完成后,通过某种机制(如回调函数)来通知主程序。
setTimeout 是一个典型的异步 Web API。
console.log("任务 1:开始");
// 这是一个异步操作setTimeout(function () { // 这个函数被称为“回调函数” console.log("任务 2:一个异步操作完成");}, 2000); // 2000 毫秒后执行
console.log("任务 3:结束");
// 输出顺序:// 任务 1:开始// 任务 3:结束// (等待 2 秒后)// 任务 2:一个异步操作完成在这个例子中,setTimeout 被调用后,JavaScript 引擎并不会等待 2 秒,而是立即执行后面的 console.log("任务 3:结束")。它把回调函数(打印 “任务 2” 的那个函数)交给了浏览器内核去计时,2 秒后当计时完成,这个回调函数才会被放回 JavaScript 的任务队列中等待执行。
回调函数是实现异步编程最基础的方式。一个回调函数就是被作为参数传递给另一个函数,并在那个函数执行完毕后被调用的函数。
上面的 setTimeout 就是一个典型的例子。在实际开发中,我们经常用它来处理网络请求。
function fetchData(url, callback) { console.log("正在从 " + url + " 获取数据..."); // 模拟网络请求的延迟 setTimeout(() => { const data = { userId: 1, content: "这是一些数据" }; console.log("数据获取成功!"); callback(data); // 成功后,调用回调函数并传入数据 }, 1500);}
// 定义一个回调函数来处理获取到的数据function handleData(data) { console.log("处理数据:", data);}
// 调用 fetchData,并传入回调函数fetchData("https://api.example.com/data", handleData);
// 输出:// 正在从 https://api.example.com/data 获取数据...// (等待 1.5 秒)// 数据获取成功!// 处理数据: { userId: 1, content: '这是一些数据' }当多个异步操作相互依赖时,如果继续使用回调函数,就容易形成层层嵌套的结构,代码难以阅读和维护。这就是所谓的“回调地狱”。
假设我们需要执行三个连续的异步请求:
// 伪代码示例fetchUser( "user123", function (user) { console.log("获取到用户:", user);
fetchPosts( user.id, function (posts) { console.log("获取到帖子:", posts);
fetchComments( posts[0].id, function (comments) { console.log("获取到评论:", comments);
// 如果还有下一步... // fetchSomethingElse(comments[0].id, function(result) { // // ... // }); }, function (error) { // fetchComments 的错误处理 console.error("获取评论失败:", error); }, ); }, function (error) { // fetchPosts 的错误处理 console.error("获取帖子失败:", error); }, ); }, function (error) { // fetchUser 的错误处理 console.error("获取用户失败:", error); },);这种代码结构:
为了解决回调地狱的问题,ES6 引入了 Promise 对象。