[特殊字符]披萨还没到你就吃了?”JavaScript异步编程入门全解[特殊字符]
JavaScript 是一门以单线程为核心的语言,这意味着它一次只能做一件事。异步编程。我们每天都在写异步代码,比如发起网络请求、操作 DOM、处理用户交互、甚至只是一个简单的setTimeout。什么是同步、什么是异步?回调函数为什么容易“嵌套地狱”?Promise 为什么被称为“异步编程的救星”?async/await 又是怎么做到让异步代码像同步代码一样优雅?很多人初学 JavaScript
前言:JavaScript 异步编程,从“懵”到“懂”
JavaScript 是一门以单线程为核心的语言,这意味着它一次只能做一件事。然而,正是这个“一次只能做一件事”的限制,催生了 JavaScript 最强大也最让人头疼的特性之一:异步编程。
我们每天都在写异步代码,比如发起网络请求、操作 DOM、处理用户交互、甚至只是一个简单的 setTimeout
。但你是否真正理解:
- 什么是同步、什么是异步?
- 回调函数为什么容易“嵌套地狱”?
- Promise 为什么被称为“异步编程的救星”?
- async/await 又是怎么做到让异步代码像同步代码一样优雅?
很多人初学 JavaScript 时,都会从最简单的同步代码入手。但一旦碰到异步逻辑,就容易陷入“执行顺序搞不清、回调嵌套写不明白、Promise 不知道怎么链式调用”的困惑。
这篇文章,就是为你写的。
这一期我们将从最基础的 同步与异步 讲起,带你一步步理解 JavaScript 是如何处理“等待”这件事的。然后在下一期我们会深入讲解 Promise 的原理与用法,最后升华到 async/await 的优雅写法,帮助你从“写异步代码”变成“写好异步代码”。
无论你是刚入门的新手,还是已经写过几年代码但对异步机制始终“似懂非懂”的开发者,只要你愿意静下心来读完这篇文章,相信你都能收获一份“豁然开朗”的理解。
同步?异步?
在 JavaScript 中,代码的执行方式可以分为两种:同步(Synchronous) 和 异步(Asynchronous) 。这两种方式就像两个人做事情的方式一样,一个“按顺序一步步来”,一个“边做边等”不耽误进度。
同步(Synchronous)
同步就是我们最熟悉、最直观的执行方式:按顺序一行一行地执行代码,前一行没执行完,后一行就得等着。 我们平时熟悉的,一般都是同步代码。
console.log("第一步"); console.log("第二步"); console.log("第三步"); // 按顺序输出
就像这一段代码一样,引擎从上到下逐步执行语句,每一句代码的执行都必须等上一句代码执行完毕,这就是同步。
异步(Asynchronous)
但现实世界中,并不是每件事都得“等前面做完才行”,比如:
你点了一份外卖,总不能一直站在门口等它送过来吧?你可以一边等,一边刷会儿手机。
JavaScript 在浏览器中面对类似场景(比如网络请求、计时器、文件读取等耗时操作)时,也采用了这种“边做边等”的方式,就是异步。
var pizza; function getPizza() { setTimeout(() => { pizza = '🍕', console.log(`${pizza}到了`) }, 2000) } console.log('开始订披萨') getPizza(); console.log('玩会手机') // 执行结果: 开始订披萨 // 玩会手机 // 披萨到了
虽然 setTimeout
写在中间,但它并不会阻止后面的代码执行。2 秒后才会输出“🍕到了”。这说明 JavaScript 的代码执行是非阻塞的,这就是异步的核心思想。
简单来说,同步/异步的执行就像下图一样:
假设这两条是程序运行的线路,代表着程序从开始到终止的全过程。
这两条线路就是负责处理同步/异步任务的不同线路,当执行到异步任务时就扔出去,交给另一条线路处理,自己继续处理剩下的任务。
就像你在吃大餐的时候,你讨厌吃海鲜,但是你的朋友喜欢吃海鲜,当你吃着吃着吃到了海鲜的时候,你就把盘子里的海鲜给了他,让他来解决,然后你继续吃剩下的饭。
为什么 JavaScript 要用异步?
JavaScript 最初设计时就是单线程语言,也就是说它同时只能做一件事。如果所有的操作都同步执行,那遇到一个耗时操作(比如读取文件、发送网络请求),整个页面就会“卡住”,用户体验极差。
相信你也不想在抢票的时候页面卡上个几分钟吧hhhhhhhhh~
所以 JavaScript 引入了 异步机制,通过事件循环(Event Loop) 来处理异步任务,让程序在等待某些操作完成的同时,可以继续执行其他任务,从而提高效率、提升用户体验。
你写的内容整体上逻辑清晰、案例生动,已经非常接近一篇优秀的技术讲解文章了。但正如你自己所说,标题和部分内容表达略显混乱,重点不突出,可以进一步优化结构和语言表达,使其更具条理性、逻辑性,也更容易理解。
如何正确使用异步编程?
大家已经对 同步与异步 有了基本认识。那么,如何在实际开发中正确地使用异步编程呢?
我们来看一个生活化的场景:你想要订一个披萨,然后打电话叫朋友 Fade,之后一起享用披萨。
这个场景中,取披萨是一个耗时的操作(异步),而打电话和吃披萨是后续的同步操作。我们要确保:在披萨送到之后,才执行吃披萨的动作。
异步与同步代码混用导致逻辑错误
我们尝试用同步思维来写这段代码:
var pizza; function getPizza() { setTimeout(() => { pizza = '🍕'; console.log(`${pizza}到了`); }, 2000); } console.log('开始订披萨'); getPizza(); console.log('Call Fade'); console.log(`Eat ${pizza} with Fade`);
输出结果是:
开始订披萨 Call Fade Eat undefined with Fade 🍕到了
我们可以看到:在披萨还没送到的时候,就已经执行了“吃披萨”的操作,这显然是不合理的。
解决方案:使用回调函数(Callback)
为了解决这个问题,我们需要一种机制,来保证:只有在异步任务完成后,才执行后续操作。
JavaScript 最早的解决方案是:使用 回调函数(Callback)。
什么是回调函数?
回调函数 是一个函数,作为参数传给另一个函数,并在该函数内部被调用。它常用于处理 异步操作,例如:网络请求、定时任务、文件读取等。
使用回调函数重构代码
我们把“吃披萨”这个动作封装成函数,并传入到 getPizza
中,确保它在披萨送达后再执行:
var pizza; function getPizza(callback) { setTimeout(() => { pizza = '🍕'; console.log(`${pizza}到了`); callback(); // 关键:表示披萨已经准备好,可以执行后续操作 }, 2000); } function eatPizza() { console.log(`Eat ${pizza} with Fade`); } console.log('开始订披萨'); getPizza(eatPizza); console.log('Call Fade');
输出结果:
开始订披萨 Call Fade 🍕到了 Eat 🍕 with Fade
完美! 现在逻辑正确了:先订披萨 → 打电话 → 披萨送到 → 吃披萨。
回调函数的本质
回调函数的本质是:告诉异步函数“你做完这件事之后该干什么”。
通过把“下一步任务”作为函数传入当前任务,我们可以实现异步操作之间的顺序控制。
回调地狱
回调地狱(Callback Hell) ,又称为“回调金字塔(Pyramid of Doom) ”,指的是多个嵌套的回调函数形成的代码结构,导致代码难以阅读、维护和调试。
再拿上面的披萨场景举例子:
function thing1(){ console.log('拿起电话'); } function thing2(){ console.log('打给披萨店'); } function thing3(){ console.log('点单'); } function thing4(){ console.log('吃披萨'); }
如果我想吃个披萨该怎么办呢?那就应该按照1,2,3,4
这四步去做,每一步都依靠上一步,但是每一步都是耗时的任务。 那就有:
function thing1(callback){ console.log('拿起电话'); callback() } function thing2(callback){ console.log('打给披萨店'); callback() } function thing3(callback){ console.log('点单'); callback() } function thing4(){ console.log('吃披萨'); } thing1(() => { thing2(() => { thing3(() => { thing4(); }); }); }); //输出: 拿起电话 // 打给披萨店 // 点单 // 吃披萨
这就是回调地狱,看看是不是可读性非常的差,看过去就不好理解发生了什么事情了~
下面还有个更难看的:
fs.readFile('file1.txt', 'utf8', function(err, data1) { if (err) return console.error(err); fs.readFile('file2.txt', 'utf8', function(err, data2) { if (err) return console.error(err); fs.readFile('file3.txt', 'utf8', function(err, data3) { if (err) return console.error(err); console.log(data1 + data2 + data3); }); }); });
总结
- 同步代码是线性执行的,异步代码不会阻塞主线程。
- 在异步任务中,回调函数是最原始的解决方案,能实现异步任务的顺序执行。
- 回调函数的本质是:“任务完成后应该做什么”。
- 回调容易造成代码嵌套深、可读性差的问题,即回调地狱。
- 在现代开发中,更推荐使用 Promise 和 async/await 来管理异步流程。
同步和异步是刚需,是我们在开发过程中必须要用的,目前我们只介绍了回调函数这一种变异步为同步的方法,下一期我们将介绍Promise,async/await
这两种更加好用的方法,来解决问题。
更多推荐
所有评论(0)