JavaScript中的异步请求与Fetch API
JavaScript 中的异步编程基础
为什么需要异步编程
在 JavaScript 的运行环境中,尤其是在浏览器端,很多操作是耗时的,例如网络请求、读取文件等。如果这些操作是同步执行的,那么在操作进行期间,主线程会被阻塞,导致页面失去响应,用户体验极差。而异步编程允许 JavaScript 在执行这些耗时操作时,不会阻塞主线程,使得主线程可以继续处理其他任务,比如响应用户的交互等。
回调函数(Callback)
回调函数是 JavaScript 中实现异步操作最基础的方式。当一个异步操作完成时,会调用预先传入的回调函数来处理结果。
例如,在 Node.js 中读取文件是一个异步操作,fs.readFile
函数接受一个回调函数作为参数:
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
在这个例子中,fs.readFile
开始读取文件,并不会阻塞主线程。当文件读取完成后,会调用回调函数,将可能出现的错误 err
和读取到的数据 data
作为参数传入回调函数进行处理。
回调地狱(Callback Hell)
当有多个异步操作需要顺序执行时,回调函数的嵌套会变得非常复杂,形成所谓的“回调地狱”。
例如,假设我们需要依次读取三个文件:
const fs = require('fs');
fs.readFile('file1.txt', 'utf8', (err1, data1) => {
if (err1) {
console.error(err1);
return;
}
fs.readFile('file2.txt', 'utf8', (err2, data2) => {
if (err2) {
console.error(err2);
return;
}
fs.readFile('file3.txt', 'utf8', (err3, data3) => {
if (err3) {
console.error(err3);
return;
}
console.log(data1, data2, data3);
});
});
});
这样的代码嵌套层次过多,可读性和维护性都很差。
Promise
为了解决回调地狱的问题,ES6 引入了 Promise。Promise 表示一个异步操作的最终完成(或失败)及其结果值。
一个 Promise 有三种状态:
- Pending(进行中):初始状态,既没有被兑现,也没有被拒绝。
- Fulfilled(已兑现):意味着操作成功完成,Promise 有一个值。
- Rejected(已拒绝):意味着操作失败,Promise 有一个原因(通常是错误对象)。
创建一个 Promise 示例:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('操作成功');
} else {
reject(new Error('操作失败'));
}
}, 1000);
});
promise.then((value) => {
console.log(value);
}).catch((error) => {
console.error(error);
});
在这个例子中,new Promise
接受一个执行器函数,该函数有两个参数 resolve
和 reject
。当异步操作成功时,调用 resolve
并传入结果值;当异步操作失败时,调用 reject
并传入错误对象。then
方法用于处理 Promise 被兑现的情况,catch
方法用于处理 Promise 被拒绝的情况。
Promise 链式调用
Promise 的一个强大之处在于可以进行链式调用,解决了回调地狱的问题。例如,上面依次读取三个文件的操作可以用 Promise 改写为:
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
readFile('file1.txt', 'utf8')
.then((data1) => {
console.log(data1);
return readFile('file2.txt', 'utf8');
})
.then((data2) => {
console.log(data2);
return readFile('file3.txt', 'utf8');
})
.then((data3) => {
console.log(data3);
})
.catch((error) => {
console.error(error);
});
这里使用了 util.promisify
方法将 fs.readFile
这个基于回调的函数转换为返回 Promise 的函数。通过链式调用 then
方法,可以清晰地看到异步操作的顺序执行,代码的可读性大大提高。
async/await
async/await
是 ES2017 引入的异步函数语法,它是基于 Promise 构建的更简洁的异步编程方式。async
函数总是返回一个 Promise。如果 async
函数的返回值不是 Promise,JavaScript 会自动将其包装成一个已兑现状态的 Promise。
await
只能在 async
函数内部使用,它用于暂停 async
函数的执行,等待一个 Promise 被兑现或被拒绝。当 Promise 被兑现时,await
表达式的值就是 Promise 的 resolved 值;当 Promise 被拒绝时,await
会抛出错误。
例如,用 async/await
改写上面读取三个文件的代码:
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
async function readFiles() {
try {
const data1 = await readFile('file1.txt', 'utf8');
console.log(data1);
const data2 = await readFile('file2.txt', 'utf8');
console.log(data2);
const data3 = await readFile('file3.txt', 'utf8');
console.log(data3);
} catch (error) {
console.error(error);
}
}
readFiles();
在这个例子中,readFiles
是一个 async
函数,通过 await
依次等待每个文件读取操作完成,代码看起来更像是同步代码,极大地提高了代码的可读性和可维护性。
Fetch API 简介
什么是 Fetch API
Fetch API 是 JavaScript 中用于发起网络请求的现代接口,它提供了一个更强大、更灵活且基于 Promise 的方式来处理异步网络请求,相比于传统的 XMLHttpRequest
更加简洁易用。Fetch API 不仅可以用于浏览器端,在 Node.js 中也可以通过安装 node - fetch
库来使用。
Fetch API 的基本用法
使用 fetch
函数发起一个简单的 GET 请求:
fetch('https://example.com/api/data')
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error('请求出错:', error);
});
在这个例子中,fetch
函数接受一个 URL 作为参数,返回一个 Promise。当请求成功时,then
方法会接收到一个 Response
对象。Response
对象包含了服务器返回的所有信息,例如状态码、响应头以及响应体。通常我们会根据响应体的格式,使用 response.json()
(如果响应体是 JSON 格式)、response.text()
(如果响应体是文本格式)或 response.blob()
(如果响应体是二进制大对象)等方法来解析响应体。
POST 请求
发起一个 POST 请求并发送数据:
const data = {
username: 'JohnDoe',
password: 'password123'
};
fetch('https://example.com/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then((response) => {
return response.json();
})
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error('请求出错:', error);
});
在这个例子中,fetch
函数的第二个参数是一个配置对象。method
属性指定请求方法为 POST
,headers
属性设置请求头,告诉服务器发送的数据是 JSON 格式,body
属性则是要发送的数据,需要使用 JSON.stringify
方法将 JavaScript 对象转换为 JSON 字符串。
处理响应状态码
在实际应用中,我们需要根据服务器返回的状态码来进行不同的处理。例如,当状态码为 404 时,表示资源未找到,我们可以提示用户相应的信息。
fetch('https://example.com/api/nonexistentdata')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error('请求出错:', error);
});
在这个例子中,通过 response.ok
属性来判断响应状态码是否在 200 - 299 之间,如果不在这个范围,则抛出一个错误,在 catch
块中进行统一的错误处理。
Fetch API 深入探究
自定义请求头
除了常见的 Content - Type
等请求头,我们还可以根据服务器的需求自定义请求头。例如,有些服务器可能需要在请求头中传递身份验证令牌。
const token = 'your - authentication - token';
fetch('https://example.com/api/protecteddata', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
})
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error('请求出错:', error);
});
在这个例子中,Authorization
请求头用于传递身份验证令牌,Bearer
是一种常见的令牌类型前缀。
处理响应头
Response
对象不仅包含响应体,还包含响应头信息。我们可以通过 response.headers
属性来获取响应头。
fetch('https://example.com/api/data')
.then((response) => {
console.log('响应头:', response.headers);
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error('请求出错:', error);
});
response.headers
是一个 Headers
对象,它提供了一系列方法来操作和查询响应头。例如,response.headers.get('Content - Type')
可以获取 Content - Type
响应头的值。
处理重定向
默认情况下,Fetch API 会自动跟随重定向(状态码 301、302 等)。但是,我们可以通过设置 fetch
配置对象的 redirect
属性来改变这种行为。
fetch('https://example.com/api/redirecteddata', {
redirect: 'manual'
})
.then((response) => {
if (response.redirected) {
console.log('重定向到:', response.url);
}
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error('请求出错:', error);
});
在这个例子中,redirect
属性设置为 manual
,表示手动处理重定向。当响应是重定向时,response.redirected
为 true
,我们可以通过 response.url
获取最终的重定向地址。
取消 Fetch 请求
在某些情况下,我们可能需要取消正在进行的 Fetch 请求,例如用户在请求过程中关闭了相关页面或取消了操作。在 JavaScript 中,可以使用 AbortController
和 AbortSignal
来实现取消 Fetch 请求。
const controller = new AbortController();
const signal = controller.signal;
fetch('https://example.com/api/slowdata', { signal })
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
if (error.name === 'AbortError') {
console.log('请求已取消');
} else {
console.error('请求出错:', error);
}
});
// 模拟一段时间后取消请求
setTimeout(() => {
controller.abort();
}, 5000);
在这个例子中,首先创建了一个 AbortController
对象和对应的 AbortSignal
对象,并将 signal
传递给 fetch
函数。然后,通过 setTimeout
模拟在 5 秒后调用 controller.abort()
方法来取消请求。当请求被取消时,catch
块中会捕获到一个 AbortError
错误,我们可以根据这个错误类型来判断请求是被取消了。
Fetch API 与其他异步编程方式结合
Fetch API 与 Promise 链式调用
由于 Fetch API 本身返回的是 Promise,所以它可以很方便地与 Promise 的链式调用结合。例如,我们可能需要先获取一个资源的 URL,然后再根据这个 URL 发起另一个 Fetch 请求。
function getResourceUrl() {
return new Promise((resolve, reject) => {
// 模拟异步操作获取 URL
setTimeout(() => {
resolve('https://example.com/api/data - resource');
}, 1000);
});
}
getResourceUrl()
.then((url) => {
return fetch(url);
})
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error('请求出错:', error);
});
在这个例子中,getResourceUrl
函数返回一个 Promise,在 then
方法中,当获取到 URL 后,立即使用这个 URL 发起 Fetch 请求,形成了一个 Promise 链式调用。
Fetch API 与 async/await
Fetch API 与 async/await
结合使用可以使代码更加简洁易读。例如,假设我们需要多次发起 Fetch 请求并顺序处理响应。
async function fetchDataSequentially() {
try {
const response1 = await fetch('https://example.com/api/data1');
const data1 = await response1.json();
console.log(data1);
const response2 = await fetch('https://example.com/api/data2');
const data2 = await response2.json();
console.log(data2);
const response3 = await fetch('https://example.com/api/data3');
const data3 = await response3.json();
console.log(data3);
} catch (error) {
console.error('请求出错:', error);
}
}
fetchDataSequentially();
在这个例子中,fetchDataSequentially
是一个 async
函数,通过 await
依次等待每个 Fetch 请求完成并处理响应,代码结构清晰,如同同步代码一样易于理解。
错误处理与最佳实践
Fetch API 中的错误类型
- 网络错误:当无法建立网络连接时,
fetch
返回的 Promise 会被拒绝,并抛出一个TypeError
。例如,网络断开或者 URL 格式错误等情况。
fetch('invalid - url')
.catch((error) => {
if (error instanceof TypeError) {
console.log('网络错误或 URL 格式错误:', error);
}
});
- HTTP 错误:如前文所述,当服务器返回的状态码不在 200 - 299 之间时,
fetch
返回的 Promise 不会被拒绝(默认行为),需要我们手动检查response.ok
并抛出错误。
fetch('https://example.com/api/nonexistentdata')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.catch((error) => {
if (error.message.includes('HTTP error')) {
console.log('HTTP 错误:', error);
}
});
- 解析错误:当使用
response.json()
、response.text()
等方法解析响应体失败时,会抛出错误。例如,响应体不是预期的 JSON 格式。
fetch('https://example.com/api/invalid - json - data')
.then((response) => {
return response.json();
})
.catch((error) => {
if (error instanceof SyntaxError) {
console.log('解析错误:', error);
}
});
最佳实践
- 统一错误处理:在应用中,可以创建一个全局的错误处理函数来处理所有 Fetch 请求可能出现的错误,这样可以保持代码的一致性。
function handleFetchError(error) {
if (error instanceof TypeError) {
console.log('网络错误或 URL 格式错误:', error);
} else if (error.message.includes('HTTP error')) {
console.log('HTTP 错误:', error);
} else if (error instanceof SyntaxError) {
console.log('解析错误:', error);
} else {
console.log('其他错误:', error);
}
}
async function fetchData() {
try {
const response = await fetch('https://example.com/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
handleFetchError(error);
}
}
fetchData();
- 设置合理的超时时间:为了防止 Fetch 请求长时间等待,可以设置一个合理的超时时间。结合前文提到的
AbortController
和AbortSignal
来实现。
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const signal = controller.signal;
const id = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { ...options, signal });
clearTimeout(id);
return response;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('请求超时');
}
throw error;
}
}
fetchWithTimeout('https://example.com/api/slowdata', {}, 3000)
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error('请求出错:', error);
});
在这个例子中,fetchWithTimeout
函数接受一个 URL、一个可选的配置对象和一个超时时间(默认为 5000 毫秒)。通过 setTimeout
和 AbortController
实现了请求超时功能。
- 缓存策略:对于一些不经常变化的数据,可以考虑使用缓存策略来减少不必要的网络请求,提高应用性能。可以结合
localStorage
或者浏览器的缓存机制来实现。
async function fetchCachedData(url) {
const cachedData = localStorage.getItem(url);
if (cachedData) {
return JSON.parse(cachedData);
}
const response = await fetch(url);
const data = await response.json();
localStorage.setItem(url, JSON.stringify(data));
return data;
}
fetchCachedData('https://example.com/api/staticdata')
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error('请求出错:', error);
});
在这个例子中,fetchCachedData
函数首先检查 localStorage
中是否有缓存的数据,如果有则直接返回;如果没有,则发起 Fetch 请求,获取数据后将其缓存到 localStorage
中。
通过以上对 JavaScript 中异步请求基础以及 Fetch API 的详细介绍,包括其基本用法、深入探究、与其他异步编程方式的结合以及错误处理和最佳实践,希望能帮助开发者在实际项目中更高效、准确地使用异步请求和 Fetch API 来构建强大的网络应用。