JavaScript - 异步操作和异步传染性

前言

JavaScript作为一门单线程语言,同一时间只能执行一个操作。这就意味着,在处理一些耗时操作时,程序会出现阻塞,导致 UI 无响应。为了避免这种情况,异步编程变得至关重要。异步操作可以将耗时的任务交给浏览器或运行时环境来处理,同时保持 UI 的响应性。

回调函数

回调函数是处理异步操作的最早方法之一。它在处理事件、定时器、网络请求等方面得到了广泛应用。然而,随着异步操作嵌套层次的增加,回调地狱(Callback Hell)成为了一个普遍存在的问题,降低了代码的可读性可维护性

使用回调函数处理异步操作
javascript
12345678910
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()可以捕获链中任何位置发生的错误,使错误处理变得更方便。

使用Promise处理异步操作
javascript
123456789
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
1234567891011121314
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
123456789101112131415161718
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
12345
function getName() {
const res = getUser('jiohon')
return res.name
}
  • 但是用户的信息是保存在服务器中的。所以,为了获取该值,我们需要发起异步请求
一个栗子
javascript
1234567891011121314151617
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并通过缓存的方式来处理异步请求结果,从而在后续的调用中直接使用缓存的数据。

  1. 在函数开始时,创建一个cache对象,它用于存储异步请求的状态和值。初始状态为'pending',值为null
  2. 将原始的window.fetch方法保存在oldFetch变量中,以便后面可以还原。
  3. 将全局的window.fetch方法替换为一个新的函数。在新的fetch方法中,首先判断cache的状态,如果已经有缓存的数据,则直接返回缓存的值。如果cache状态为'rejected',则抛出缓存的错误值。
  4. 如果没有缓存或缓存状态为'pending',则调用原fetch方法发起请求。接着在成功失败的情况下分别将结果保存到cache中,并改变cache的状态为'fulfilled''rejected'
  5. 在捕获到结果之前,抛出了一个p变量,它是新的fetch方法返回的Promise对象。这个步骤的目的是为了在func执行的过程中捕获到这个Promise,以便在后续的错误处理中使用。
  6. 使用try块来执行传入的func函数。
  7. 在捕获到错误时,判断错误是否是一个Promise对象。使用.then()方法再次执行func,无论是成功还是失败。最后,无论如何,都会通过.finally()来还原原始的window.fetch方法。
消除异步传染性
javascript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
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
}
)
// 抛出错误,返回当前的promise
throw p
}
try {
func()
} catch (err) {
// 捕获到抛出的primise ,判断是否为promise
if (err instanceof Promise) {
// 再次执行
err.then(func, func).finally(() => {
// 还原fetch
window.fetch = oldFetch
})
}
}
}
// run(main)