今天我们将通过建立并运行一个冰淇淋店来学习异步 JavaScript。在此过程中,你将学习如何使用:
- Callbacks
- Promises
- Async / Await
我们将在本文中介绍以下内容:
- 什么是异步 JavaScript
- JavaScript 中的同步与异步
- Callbacks 如何在 JavaScript 中运行
- Promises 如何在 JavaScript 中运行
- Async/Await 如何在 JavaScript 中运行
让我们开始吧!
如果你喜欢,也可以在 YouTube 上观看本教程。
什么是异步 JavaScript?
如果你想高效地构建项目,那么这个概念很适合你。
异步 JavaScript 理论可以帮助你将大型复杂的项目分解为较小的任务。
然后你可以使用 callbacks、promises 或 Async/await 中的任何一种来运行这些小任务,获得最好的结果。
让我们开始吧!🎖️
JavaScript 中的同步和异步
什么是同步系统?
在同步系统中,任务一个接一个地完成。
想象一下,如果你只有一只手去完成十项任务,那么在同一个时间你只能做一个任务。
看看这个动图 – 这里会发生一件事:
你将看到,直到第一个图像完全加载,第二个图像才开始加载。
JavaScript 默认是同步的 [单线程]。你可以这样想 ——— 单线程意味着一次只能做一件事。
什么是异步系统?
在这个系统中,任务是独立完成的。
假设你有十个任务以及十只手,那么在同一时间,每只手都可以同时独立完成每一项任务。
看看这张动图 - 你可以看到每个图像都是同时加载的。
同样,所有的图像都以自己的速度加载,它们都不会等待其他任务先完成。
总结一下同步 JS 和异步 JS
想象三张图片在跑马拉松,在一个:
- 同步 系统,三张图片在同一条跑道上,一个不能超过另外一个,比赛得一个接一个地完成,如果 2 号图像停止,后续的图片也会停止。
- 异步 系统,这三张图片在不同的跑道上,它们会在自己的跑道上完成比赛,不会受其他图片的影响:
同步和异步代码示例
在开始我们的项目之前,让我们看一些例子来消除一些疑问。
同步的代码示例
为了测试同步系统,用 JavaScript 写以下代码:
console.log(" I ");
console.log(" eat ");
console.log(" Ice Cream ");
控制台的结果如下:
异步代码示例
我们假设吃冰淇淋需要两秒钟。现在,让我们测试一个异步系统。用JavaScript编写下面的代码。
注意: 不用担心,我们将在本文后面讨论 setTimeout()
函数。
console.log("I");
// This will be shown after 2 seconds
setTimeout(()=>{
console.log("eat");
},2000)
console.log("Ice Cream")
控制台的结果如下:
既然我们已经了解了同步操作和异步操作之间的区别,那么让我们来创建一个冰淇淋商店。
如何设置我们的项目
对于这个项目,你只需要打开 Codepen.io直接开始编码。或者,你可以用 VS Code 编辑器来做。
打开 JavaScript 部分,然后打开开发人员控制台,我们将编写代码并在控制台中查看结果。
什么是 JavaScript 中的回调函数?
当你将一个函数作为参数嵌套到另一个函数中时,这叫作回调。
下面是一个回调的说明:
一个回调的例子
别担心,我们马上就会看到一些回调的例子。
为什么要使用回调?
在做一个复杂的任务时,我们把它分解成更小的步骤。为了根据时间(可选)和顺序在这些步骤之间建立关系,我们会使用回调。
看看这个例子:
图表包含制作冰淇淋的步骤
这些是制作冰淇淋需要的小步骤。还要注意,在本例中,步骤的顺序和计时是至关重要的,你不能只把水果切了就端上冰淇淋。
同时,如果前一个步骤没有完成,我们就不能进入下一个步骤。
为了更详细地解释这一点,让我们开始做冰淇淋店的生意。
等等...
该店将分为两部分:
- 储藏室里有所有的配料 - 后台
- 我们将在厨房里制作冰淇淋 - 前端
让我们存储数据
现在,我们要把配料存储在一个对象中。
你可以像这样在对象中存储成分:
let stocks = {
Fruits : ["strawberry", "grapes", "banana", "apple"]
}
我们的其他食材在这里:
你可以像这样将这些其他成分存储在 JavaScript 对象中:
let stocks = {
Fruits : ["strawberry", "grapes", "banana", "apple"],
liquid : ["water", "ice"],
holder : ["cone", "cup", "stick"],
toppings : ["chocolate", "peanuts"],
};
整个业务取决于客户的 订单。一接到订单,我们就开始生产,然后供应冰淇淋。因此我们将创建两个函数 ->
order
production
这就是它的工作原理:
从客户那里获取订单,取得食材,开始生产,然后上桌。
我们来写一下函数。在这里我们使用箭头函数:
let order = () =>{};
let production = () =>{};
现在,让我们使用回调建立这两个函数之间的关系,如下所示:
let order = (call_production) =>{
call_production();
};
let production = () =>{};
让我们做个小测试
我们将使用 console.log()
函数进行测试,以消除关于如何建立这两个函数之间的关系的疑问。
let order = (call_production) =>{
console.log("Order placed. Please call production")
// function 👇 is being called
call_production();
};
let production = () =>{
console.log("Production has started")
};
为了运行测试,我们将调用 order
函数。我们将添加第二个函数名为 production
作为它的参数。
// name 👇 of our second function
// 将第二个函数命名为 👇
order(production);
下面是控制台中的结果:
休息一下
到目前为止一切都很好,休息一下吧!
清除 console.log 日志
保留这段代码并删除所有的东西,不要删除我们的 stocks 变量。在我们的第一个函数中,传递另一个参数,以便我们可以接收订单,即水果名:
// 函数 1
let order = (fruit_name, call_production) =>{
call_production();
};
// 函数 2
let production = () =>{};
// 触发 👇
order("", production);
下面是我们的步骤,以及执行每个步骤所需的时间。
图表包含制作冰淇淋的步骤
在这个图表中,你可以看到第一步是下订单,这需要 2 秒。第二步是切水果(2秒),第三步是加水和冰(1秒),第四步启动机器(1秒),第五步是选择容器(2秒),第六步是选择配料(3秒),以及第七步,也就是最后一步,端上冰淇淋,这需要 2 秒。
要建立计时,函数 setTimeout()
非常好,因为它也使用一个回调函数作为参数。
setTimeout() 函数的语法
现在,让我们使用这个函数来选择水果:
// 功能1
let order = (fruit_name, call_production) =>{
setTimeout(function(){
console.log(`${stocks.Fruits[fruit_name]} was selected`)
// Order placed. Call production to start
call_production();
},2000)
};
// 功能2
let production = () =>{
// blank for now
};
// 触发 👇
order(0, production);
下面是控制台中的结果:
注意 2秒后才会显示结果。
如果你想知道我们是如何从 stock 变量中采摘草莓的,下面是代码:
不删除任何代码。现在,我们将使用以下代码开始编写生产函数。我们将使用箭头函数。
let production = () =>{
setTimeout(()=>{
console.log("production has started")
},0000)
};
结果如下:
我们将在现有的 setTimeout
函数中嵌套另一个 setTimeout
函数来切水果:
let production = () =>{
setTimeout(()=>{
console.log("production has started")
setTimeout(()=>{
console.log("The fruit has been chopped")
},2000)
},0000)
};
结果如下:
如果你还记得,这是我们的步骤:
图表包含制作冰淇淋的步骤
让我们通过在另一个函数中嵌套一个函数来完成我们的冰淇淋生产 - 这也叫作回调,还记得吗?
let production = () =>{
setTimeout(()=>{
console.log("production has started")
setTimeout(()=>{
console.log("The fruit has been chopped")
setTimeout(()=>{
console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} Added`)
setTimeout(()=>{
console.log("start the machine")
setTimeout(()=>{
console.log(`Ice cream placed on ${stocks.holder[1]}`)
setTimeout(()=>{
console.log(`${stocks.toppings[0]} as toppings`)
setTimeout(()=>{
console.log("serve Ice cream")
},2000)
},3000)
},2000)
},1000)
},1000)
},2000)
},0000)
};
控制台结果如下:
感到疑惑吗?
这叫作回调地狱,它看起来像这样(还记得上面的代码吗):
回调地狱图解
解决方案是什么?
如何使用 Promise 来避免回调地狱
Promises 的发明是为了解决回调地狱的问题和更好地处理我们的任务。
休息一下
先休息一下!
这就是 promise 的样子
promise 的格式说明
让我们一起来剖析 promise。
promise 周期的图解
如上图所示,一个 promise 有三种状态:
- Pending: 这是初始阶段,这里什么也没有发生。你可以这样想,你的客户正在慢慢地给你下订单,但是他们还没有点任何东西。
- Resolved: 这意味着你的顾客已经收到了他们的食物并且很高兴
- Rejected: 这意味着你的顾客没有收到他们点的单并离开了冰激凌店
让我们将 promise 应用到我们的冰淇淋生产案例研究中。
等等
首先,我们需要了解另外四件事 ->
- 时间和工作的关系
- Promise 链
- 错误处理
.finally
函数
让我们开始我们的冰淇淋店,一步一步地理解这些概念。
时间和工作的关系
如果你还记得,这就是我们制作冰淇淋的步骤和时间
图表包含制作冰淇淋的步骤
为了实现这一点,让我们在 JavaScript 中创建一个变量:
let is_shop_open = true;
现在创建一个名叫 order
的函数,然后传两个名叫 time, work
的参数:
let order = ( time, work ) =>{
}
现在,我们要向客户发起 promise,"我们将给献上冰淇淋",如下 ->
let order = ( time, work ) =>{
return new Promise( ( resolve, reject )=>{ } )
}
我们的 promise 有两个部分:
- Resolved - 用户拿到了冰激凌
- Rejected - 用户没有拿到冰激凌
let order = ( time, work ) => {
return new Promise( ( resolve, reject )=>{
if( is_shop_open ){
resolve( )
}
else{
reject( console.log("Our shop is closed") )
}
})
}
让我们在 if
语句中使用 setTimeout()
函数在 promise 中添加时间和工作因素。
注意: 在现实生活中,你也可以避免时间因素,这完全取决于你的工作性质。
let order = ( time, work ) => {
return new Promise( ( resolve, reject )=>{
if( is_shop_open ){
setTimeout(()=>{
// work is 👇 getting done here
resolve( work() )
// Setting 👇 time here for 1 work
}, time)
}
else{
reject( console.log("Our shop is closed") )
}
})
}
现在,我们要用新创建的函数开始制作冰淇淋。
// Set 👇 time here
order( 2000, ()=>console.log(`${stocks.Fruits[0]} was selected`))
// pass a ☝️ function here to start working
2 秒后的结果是:
很棒!
Promise 链
在这个方法中,我们使用 .then
处理后续的程序:
使用 .then 处理函数的 promise 链说明
当我们的 promise 被 resolve 时, .then 处理函数返回一个 promise。
例子如下:
让我说得简单点:这类似于给某人指示,你告诉别人“先做这个,然后做那个,然后做其他的事情,然后…”,然后……,然后……”等。
- 他的首要任务是我们原始 promise
- 一旦完成了一小部分工作,剩下的任务就返回了新的 promise
让我们在项目中实现这一点。在代码的底部编写以下代码行。
注意: 不要忘记在 .then
函数中写 return
。否则,它将不能正常工作。如果你很好奇,试着在我们完成这些步骤后去掉返回值:
order(2000,()=>console.log(`${stocks.Fruits[0]} was selected`))
.then(()=>{
return order(0000,()=>console.log('production has started'))
})
结果如下:
使用相同的系统,让我们完成我们的项目:
// step 1
order(2000,()=>console.log(`${stocks.Fruits[0]} was selected`))
// step 2
.then(()=>{
return order(0000,()=>console.log('production has started'))
})
// step 3
.then(()=>{
return order(2000, ()=>console.log("Fruit has been chopped"))
})
// step 4
.then(()=>{
return order(1000, ()=>console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} added`))
})
// step 5
.then(()=>{
return order(1000, ()=>console.log("start the machine"))
})
// step 6
.then(()=>{
return order(2000, ()=>console.log(`ice cream placed on ${stocks.holder[1]}`))
})
// step 7
.then(()=>{
return order(3000, ()=>console.log(`${stocks.toppings[0]} as toppings`))
})
// Step 8
.then(()=>{
return order(2000, ()=>console.log("Serve Ice Cream"))
})
结果如下:
错误处理
当出现错误时,我们需要一种处理错误的方法。但首先,我们需要了解 promise 周期。
promise 周期说明
为了捕获错误,让我们将变量改为 false。
let is_shop_open = false;
也就是说我们的店关门了,我们不再卖冰淇淋给顾客了。
为了处理这种情况我们使用 .catch
函数,类似 .then
,它也返回一个promise,但只有当我们最初的 promise 被 reject 时才会执行。
这里有一个小提示:
.then
在 promise resolved 时候被执行.catch
在 promise rejected 时候被执行
到代码最底部,编写以下代码:
记住在.then
和 .catch
之间不能有任何东西。
.catch(()=>{
console.log("Customer left")
})
结果如下:
关于这段代码,有两点需要注意:
- 第一个信息是从
reject()
部分来的 - 第二个信息是从
catch()
部分来的
如何使用 .finally() 函数
有一个叫作 “finally” 的函数,不管我们的 promise 是被 resolve 了还是被 reject 了,它都会被执行。
例如: 不管我们是没有顾客还是有 100 个顾客,我们的店都会在一天结束的时候关门。
如果您想对此进行测试,请在最下面编写以下代码:
.finally(()=>{
console.log("end of day")
})
结果如下:
请大家欢迎 Async/Await~
Async/Await 如何在 JavaScript 中运行
这应该是编写 promise 的更好方式,它可以帮助我们保持代码的简单和干净。
你所要做的就是在任何常规函数之前写 async
关键字,它就变成了一个 promise。
先休息一下
让我们来看一看:
JavaScript 中 Promises vs Async/Await
在 async/await 之前,为了写一个 promise,我们这样写:
function order(){
return new Promise( (resolve, reject) =>{
// Write code here
} )
}
现在使用 async/await,我们可以这么写:
//👇 神奇的关键字
async function order() {
// Write code here
}
等等...
你必须理解->
- 如何使用
try
和catch
关键字 - 如何使用
await
关键字
如何使用 Try 和 Catch 关键字
我们使用 try
关键字来运行代码,同时使用 catch
来捕获错误。这和我们看 promise 时看到的概念是一样的。
让我们来比较一下。我们来看一个小 demo,然后开始编码。
JS 中的 Promise -> resolve 和 reject
我们在 resolve 中这样使用 resolve 和 reject:
function kitchen(){
return new Promise ((resolve, reject)=>{
if(true){
resolve("promise is fulfilled")
}
else{
reject("error caught here")
}
})
}
kitchen() // run the code
.then() // next step
.then() // next step
.catch() // error caught here
.finally() // end of the promise [optional]
JS 中的 Async/Await -> try, catch
当我们使用 async/await 时,可以这么写:
//👇 神奇的关键字
async function kitchen(){
try{
// 我们来制造一个假问题
await abc;
}
catch(error){
console.log("abc does not exist", error)
}
finally{
console.log("Runs code anyways")
}
}
kitchen() // 调用
不要慌,我们接下来将讨论 await
关键字。
现在希望你理解了 promise 和 async/await 之间的区别了。
如何使用 JavaScript 的 Await 关键字
关键字 await
使 JavaScript 等待,直到一个 promise reslove 时才会返回它的结果。
如何在 JavaScript 中使用 await 关键字
我们回冰淇淋店去吧。我们不知道顾客更喜欢哪种配料,巧克力还是花生,所以我们需要停止机器,然后去问顾客他们想在冰淇淋上加什么。
注意这里只有我们的厨房被停止了,但是我们在厨房外的员工仍然会做这样的事情:
- 洗餐具
- 清洁桌子
- 点单,等等
一个 Await 关键字代码示例
让我们创建一个小 promise 来询问要使用那种配料,这个过程需要 3 秒。
function toppings_choice (){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve( console.log("which topping would you love?") )
},3000)
})
}
现在,让我们首先使用 async 关键字来创建 kitchen 函数。
async function kitchen(){
console.log("A")
console.log("B")
console.log("C")
await toppings_choice()
console.log("D")
console.log("E")
}
// 触发函数
kitchen();
让我们在 kitchen()
调用下面添加其他任务。
console.log("doing the dishes")
console.log("cleaning the tables")
console.log("taking orders")
结果如下:
我们走出厨房问我们的顾客,“你想要哪种配料?”,与此同时,还有其他事情要做。
一旦他们选好了配料,我们就进入厨房,完成任务。
注意
当使用 Async/Await 时,你也可以使用 promise 的核心部分 .then
、.catch
和 .finally
函数。
我们再开一家冰淇淋店吧
我们要创建两个函数 ->
kitchen
: 制作冰激凌time
: 分配好每一项小任务所需要的时间
让我们开始吧!先创建时间函数:
let is_shop_open = true;
function time(ms) {
return new Promise( (resolve, reject) => {
if(is_shop_open){
setTimeout(resolve,ms);
}
else{
reject(console.log("Shop is closed"))
}
});
}
现在,让我们创建我们的厨房:
async function kitchen(){
try{
// instruction here
}
catch(error){
// error management here
}
}
// Trigger
kitchen();
让我们来做个小说明,看看我们的厨房功能是否正常:
async function kitchen(){
try{
// 执行这 1 个任务所花费的时间
await time(2000)
console.log(`${stocks.Fruits[0]} was selected`)
}
catch(error){
console.log("Customer left", error)
}
finally{
console.log("Day ended, shop closed")
}
}
// 触发
kitchen();
当商店开门时,结果是这样的:
当商店关门时,结果是这样的:
到目前为止一切顺利。
让我们完成我们的项目。
下面是我们的任务列表:
图表包含制作冰淇淋的步骤
首先,开张:
let is_shop_open = true;
现在在 kitchen()
函数中编写步骤:
async function kitchen(){
try{
await time(2000)
console.log(`${stocks.Fruits[0]} was selected`)
await time(0000)
console.log("production has started")
await time(2000)
console.log("fruit has been chopped")
await time(1000)
console.log(`${stocks.liquid[0]} and ${stocks.liquid[1]} added`)
await time(1000)
console.log("start the machine")
await time(2000)
console.log(`ice cream placed on ${stocks.holder[1]}`)
await time(3000)
console.log(`${stocks.toppings[0]} as toppings`)
await time(2000)
console.log("Serve Ice Cream")
}
catch(error){
console.log("customer left")
}
}
结果如下:
总结
恭喜你读完了本文!在本文中,你可以了解到:
- 同步和异步系统之间的区别
- 异步 JavaScript 使用 3 种机制(callbacks、promises 和 Async/Await)
这是你阅读到最后的奖励。❤️
欢迎提出建议和批评
YouTube / Joy Shaheb
LinkedIn / JoyShaheb
Twitter / JoyShaheb
Instagram / JoyShaheb
致谢
- Collection of all the images used
- Unicorns, kitty avatar
- tabby cat, Astrologist Woman, girl-holding-flower
- Character emotions
原文:JavaScript Async/Await Tutorial – Learn Callbacks, Promises, and Async/Await in JS by Making Ice Cream 🍧🍨🍦,作者:Joy Shaheb