JavaScript - 异步操作和异步传染性
#前言
JavaScript
作为一门 单线程语言
,同一时间只能执行一个操作。这就意味着,在处理一些耗时操作时,程序会出现 阻塞
,导致 UI 无响应。为了避免这种情况, 异步编程
变得至关重要。 异步操作
可以将耗时的任务交给浏览器或运行时环境来处理,同时保持 UI 的响应性。
#回调函数
回调函数
是处理异步操作的最早方法之一。它在处理事件、定时器、网络请求等方面得到了广泛应用。然而,随着异步操作嵌套层次的增加, 回调地狱(Callback Hell)
成为了一个普遍存在的问题,降低了代码的 可读性
和 可维护性
。
使用回调函数处理异步操作JavaScript
function fetchData(url, callback) {fetch(url).then((response) => response.json()).then((data) => callback(data)).catch((error) => console.error(error))}fetchData('https://api.github.com/users/jiohon', function (data) {console.log(data)})
#Promise 和异步操作链
为了解决回调地狱问题, ES6
引入了 Promise
,它提供了一种更优雅的处理异步操作的方式。 Promise
允许我们将异步操作组合成 链式调用
,通过 .then()
来处理成功和失败的情况,使得代码更具结构性。同时,通过 .catch()
可以捕获链中任何位置发生的错误,使错误处理变得更方便。
MDN文档对Promise的定义是:一个 Promise
对象代表一个在这个 Promise
被创建出来时不一定已知的值。它让我们能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。
使用Promise处理异步操作JavaScript
function fetchData(url) {return fetch(url).then((response) => response.json()).catch((error) => console.error(error))}fetchData('https://api.github.com/users/jiohon').then((data) => console.log(data)).catch((error) => console.error(error))
#async/await
ES2017
引入了 async/await
语法。使用 async
关键字可以标记一个函数为 异步函数
,而使用 await
关键字可以等待一个异步操作完成。这种语法使异步代码看起来更像是 同步代码
,极大地提高了可读性。
使用async/await处理异步操作JavaScript
async function fetchData(url) {try {const response = await fetch(url)const data = await response.json()return data} catch (error) {console.error(error)}};(async () => {const data = await fetchData('https://api.github.com/users/jiohon')console.log(data)})()
#错误处理和异常
在异步操作中, Promise
提供了错误传播机制,可以通过 .catch()
来捕获和处理错误。同时, async/await
也可以使用 try/catch
来捕获异步操作中的异常,使得错误处理更加直观。
使用async/await处理错误JavaScript
async function fetchData(url) {try {const response = await fetch(url)const data = await response.json()return data} catch (error) {console.error(error)}};(async () => {try {const data = await fetchData('https://api.github.com/users/jiohon')console.log(data)} catch (error) {console.error(error)}})()
#异步传染性
异步传染性
是指当一个函数变成异步后,所有调用它的函数也需要变成异步,这种"传染"会沿着调用链一直向上传播。
- 假设我们有一个函数
getUser
,传入用户标识后,查找该用户信息,并且返回用户名。
一个简单示例JavaScript
function getName() {const res = getUser('jiohon')return res.name}
- 但是
用户的信息
是保存在服务器中的。所以,为了获取该值,我们需要发起异步请求
。
异步传染示例JavaScript
function getUser(user) {return fetch(`https://api.github.com/users/${user}`)}async function getName() {const res = await getUser('jiohon')const user = await res.json()return user.name}async function main() {const res = await getName()console.log(res) // hushhhh}main()
但是,
async await
是有传染性
的,当一个函数变为async
后,这意味着调用他的函数也需要是async
,这破坏了getName
的同步特性。
#消除异步传染性
消除异步的 传染性
是指在 JavaScript 中处理异步代码时,防止异步操作在代码中传播,影响到其他部分的执行。这对于维护代码的 可 预测性
和 可测试性
非常重要。
利用 try...catch
并通过 缓存
的方式来处理异步请求结果,从而在后续的调用中直接使用缓存的数据。
- 在函数开始时,创建一个
cache
对象,它用于存储异步请求的状态和值。初始状态为'pending'
,值为null
。 - 将原始的
window.fetch
方法保存在oldFetch
变量中,以便后面可以还原。 - 将全局的
window.fetch
方法替换为一个新的函数。在新的fetch
方法中,首先判断cache
的状态,如果已经有缓存的数据,则直接返回缓存的值。如果cache
状态为'rejected'
,则抛出缓存的错误值。 - 如果没有缓存或缓存状态为
'pending'
,则调用原fetch
方法发起请求。接着在成功
和失败
的情况下分别将结果保存到cache
中,并改 变cache
的状态为'fulfilled'
或'rejected'
。 - 在捕获到结果之前,抛出了一个
p
变量,它是新的fetch
方法返回的Promise
对象。这个步骤的目的是为了在func
执行的过程中捕获到这个Promise
,以便在后续的错误处理中使用。 - 使用
try
块来执行传入的func
函数。 - 在捕获到错误时,判断错误是否是一个
Promise
对象。使用.then()
方法再次执行func
,无论是成功
还是失败
。最后,无论如何,都会通过.finally()
来还原原始的window.fetch
方法。
消除异步传染性的技术方案JavaScript
function run(func) {const cache = {status: 'pending',value: null,}const oldFetch = window.fetch// 修改请求逻辑window.fetch = function (...args) {// 判断是否有缓存if (cache.status === 'fulfilled') {return cache.value} else if (cache.status === 'rejected') {throw cache.value}// 发起请求,then或catch后保存数据const p = oldFetch(...args).then((res) => res.json())// 保存值到缓存,并改变状态.then((res) => {cache.status = 'fulfilled'cache.value = res},(err) => {cache.status = 'rejected'cache.value = err})// 抛出错误,返回当前的promisethrow p}try {func()} catch (err) {// 捕获到抛出的primise ,判断是否为promiseif (err instanceof Promise) {// 再次执行err.then(func, func).finally(() => {// 还原fetchwindow.fetch = oldFetch})}}}// run(main)
#总结
异步编程
是 JavaScript 中不可避免的一部分,它使我们能够处理耗时操作而不阻塞主线程。通过使用 回调函数
、 Promise
和 async/await
,我们可以更优雅地管理异步操作。然而, 异步传染性
可能会导致代码结构的复杂化,特别是在与同步代码混合时。通过本文介绍的技术方案,我们可以在特定场景下消除或减轻异步传染,使代码更加清晰和可维护。