从回调地狱到Promise乐土:我实现了异步代码的优雅转型

1,597 阅读6分钟

javascript的执行机制

今天玖玖passion就带大家来聊一聊js这门语言的运行机制!!准备好了吗 准备发车!啦!

07dc7912a2ba04b048920f1a3b5e370.jpg JavaScript的运行环境是单线程的,这意味着在同一时间只能执行一个任务。当遇到需要耗时(异步)的代码,那就先挂起,先执行不耗时的代码,v8腾出手来了再执行耗时的代码。在实际开发中我们会 遇到许多异步操作,如何处理这些异步操作是我们要翻越的一座大山!!

异步与同步

同步代码的执行是按顺序的,即一条语句在另一条语句开始执行之前必须完全完成。在同步模式下,程序的执行流是阻塞的,直到当前操作完成。这意味着如果一个操作需要时间来完成(例如读取文件、执行网络请求),那么在该操作完成之前,后续的代码不会执行。那么异步呢? 异步代码(需要耗时)允许在等待一个操作完成时不阻碍程序的执行。这意味着在发起一个可能需要时间的请求后,JavaScript引擎会继续执行接下来的代码,而不是等待该操作完成。一旦异步操作完成,可以通过回调函数、事件监听器、Promise或async/await等方式来处理结果。

常见的异步场景

在JavaScript中,异步编程主要用于处理那些可能需要较长时间才能完成且不应阻塞主线程的操作。以下是一些常见的异步场景:

  1. 定时器(Timers) :

    • setTimeout(): 用于在指定的时间延迟后执行函数。
    • setInterval(): 用于每隔指定的时间间隔重复执行函数。
  2. 事件监听器(Event Listeners) :

    • 监听用户交互事件(如点击、滚动、键盘输入)或系统事件(如窗口调整大小、加载完成)。
  3. DOM 操作:

    • 在某些情况下,DOM 操作可能间接触发异步行为,比如修改DOM可能触发浏览器的重排或重绘,但这通常不是直接由JS控制的异步。
  4. AJAX 请求:

    • 使用XMLHttpRequest或Fetch API从服务器异步获取数据。
  5. Web Workers:

    • 允许在后台线程中执行脚本,避免阻塞UI线程。
  6. Service Workers:

    • 可以在没有用户交互的情况下运行,处理推送通知和缓存更新。
  7. Promises 和 Async/Await:

    • 用于组织和处理异步操作的结果,提供更清晰的错误处理和链式调用能力。
  8. File APIs:

    • 文件读取和写入操作通常是非阻塞的。
  9. WebSocket:

    • 提供全双工通信通道,可以在客户端和服务器之间双向发送消息。
  10. Blob 和 FileReader:

    • 用于处理二进制数据,如图像或视频文件。
  11. 生成器(Generators)和 Async Generators:

    • 用于创建可暂停和恢复的函数,常用于处理流式数据。
  12. Node.js 的 I/O 操作:

    • 如果你正在使用Node.js,那么文件系统操作、数据库查询、网络请求等都是异步的。

这些异步场景的核心在于,它们允许JavaScript代码在等待某个操作完成时继续执行其他任务,而不是阻塞程序直到该操作完成。这是现代Web应用流畅体验的关键因素之一。t

js中常见的异步处理方法

回调函数(Callback)

回调函数是最原始的异步处理方式。当一个异步操作完成时,会调用一个预先定义的函数,即回调函数。但是,当异步操作嵌套过多时,会导致“回调地狱”(Callback Hell),使得代码难以阅读和维护在实际开发中我们总会遇到这种情况——b函数需要的值要等到a函数执行完之后才能获取,而前提条件又是a是异步的b是同步的,这样b函数就得不到a执行后的值了。那么如何处理这种异步操作呢?以下代码将会演示callback的效果!!

let data=null  
function a(){  
setTimeout(()=>{  
data={name:'牛哥'}  
b()  
},1000)  
}  
  
function b(){  
console.log(data.name+'好帅');  
}  
  
a()

结果如下:

image.png 在这种情况下我们相当于把异步捋成同步了,但是这样的缺点也很明显。我们可以想象一下当项目非常 庞大的时候,有上百个函数,此时b函数依赖a函数执行的结果,c函数依赖b函数执行的结果......

function a(){  
b()  
}  
function b(){  
c()  
}  
function c(){  
d()  
}  
function d(){  
e()  
}  
function e(){  
  
}  
a()

我们可以想象假如后边又紧跟着100甚至是1000个函数呢?此时我们就陷入了回调地狱(嵌套过深出现问题很难排查)这些代码我们很难去维护,假如更改了一个函数的话后边的函数全都需要去更改。这就相当痛苦了~~

promise

我们先来聊一聊promise Promise是JavaScript中用于处理异步操作的一种机制,它为异步编程提供了一种更清晰、更易于理解和维护的解决方案,相比于传统的回调函数(callback)或事件监听器。Promise的设计旨在解决“回调地狱”(Callback)的问题,即嵌套多层的回调函数导致的代码可读性和可维护性下降。

function xq(){  
setTimeout(()=>{  
console.log('徐总相亲了')  
},2000)  
}  
function marry(){  
setTimeout(()=>{  
console.log('徐总结婚了')  
},1000)  
}  
xq()  
marry()

执行结果

image.png 我们多么希望执行结果为先相亲后结婚啊!!我们固然可以用回调,但今天我想聊一聊promise这种执行机制!!

function xq(){  
return new Promise((resolve,reject)=>{  
setTimeout(()=>{  
console.log('徐总相亲了')  
resolve()  
},2000)  
})  
}  
  
function marry(){  
setTimeout(()=>{  
console.log('徐总结婚了')  
},1000)  
}  
xq().then(()=>{  
marry()  
})

resolve函数用于指示Promise对象的异步操作成功完成。通常在异步操作成功执行后,我们可以调用resolve函数,并可以向resolve传递一个参数,这个参数将成为.then()方法中成功回调函数的参数。 同理reject函数用于指示Promise对象的异步操作失败,通常在异步操作出现错误或未能成功执行时,我们可以调用reject函数,并可以向reject传递一个参数,这个参数通常是表示错误的对象,但也可以是任何值。 xq这个函数被调用时候会返回一个Promise,自带一个回调函数,可带有resolve和reject两个参数。 当有多个操作的时候呢?请看下面代码

function xq(){  
return new Promise((resolve,reject)=>{  
setTimeout(()=>{  
console.log('徐总相亲了')  
resolve()  
},2000)  
})  
}  
  
function marry(){  
return new Promise((resolve,reject)=>{  
setTimeout(()=>{  
console.log('徐总结婚了')  
resolve()  
},1000)  
})  
}  
function baby(){  
console.log('小徐出生了')  
}  
xq().then(()=>{  
return marry()  
}).then(()=>{  
baby()  
})

运行结果如下 image.png 我们可以从下面代码看出

xq().then(()=>{  
return marry()  
}).then(()=>{  
baby()  
})

我们使用Promise链来安排一系列异步操作的执行顺序,第一个.then方法返回的Promise(由marry函数创建)解析后,链式调用的第二个.then方法的回调函数将被调用。这样可以对比发现 我们不用再写回调了,不用再把一个函数体放在另外一个函数体中调用了。直接靠以上操作把异步捋成了同步啦

bc23572f1bfff36a06214ead5e91336.jpg

感谢大家观看!!请点赞加转发~~