JavaScript异步编程最佳实践:从回调到async/await的完整指南#
JavaScript的异步编程是现代Web开发的核心技能。从早期的回调函数到Promise,再到async/await,JavaScript的异步编程模式经历了显著的演进。本文将深入探讨这些模式,并提供实际开发中的最佳实践。
异步编程的演进历程#
1. 回调函数时代#
基础回调模式
1
2
3
4
5
6
7
8
9
10
| // 简单的异步回调
function fetchData(callback) {
setTimeout(() => {
callback('Hello, World!')
}, 1000)
}
fetchData((result) => {
console.log(result)
})
|
回调地狱问题
1
2
3
4
5
6
7
8
9
10
| // 回调地狱示例
fetchUser((user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
processComments(comments, (result) => {
console.log('处理完成:', result)
})
})
})
})
|
回调解决方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // 使用命名函数避免回调地狱
function fetchUser(callback) {
setTimeout(() => {
callback({ id: 1, name: 'John' })
}, 1000)
}
function fetchPosts(userId, callback) {
setTimeout(() => {
callback([{ id: 1, title: 'Post 1' }])
}, 1000)
}
function fetchComments(postId, callback) {
setTimeout(() => {
callback(['Comment 1', 'Comment 2'])
}, 1000)
}
function processComments(comments, callback) {
setTimeout(() => {
callback(comments.length)
}, 1000)
}
// 线性回调链
fetchUser((user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
processComments(comments, (result) => {
console.log('处理完成:', result)
})
})
})
})
|
2. Promise时代#
基础Promise
1
2
3
4
5
6
7
8
9
10
11
12
| // 创建Promise
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello, World!')
}, 1000)
})
}
fetchData()
.then(result => console.log(result))
.catch(error => console.error(error))
|
Promise链式调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| function fetchUser() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: 1, name: 'John' })
}, 1000)
})
}
function fetchPosts(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([{ id: 1, title: 'Post 1' }])
}, 1000)
})
}
function fetchComments(postId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(['Comment 1', 'Promise Comment 2'])
}, 1000)
})
}
// Promise链避免回调地狱
fetchUser()
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => processComments(comments))
.then(result => console.log('处理完成:', result))
.catch(error => console.error('错误:', error))
|
Promise静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| // Promise.all - 并行执行,全部成功才成功
const promises = [
fetchData('/api/user'),
fetchData('/api/posts'),
fetchData('/api/comments')
]
Promise.all(promises)
.then(results => {
console.log('所有请求成功:', results)
})
.catch(error => {
console.error('有请求失败:', error)
})
// Promise.race - 竞争执行,第一个完成就返回
const racePromises = [
fetchData('/api/fast'),
fetchData('/api/slow'),
fetchData('/api/medium')
]
Promise.race(racePromises)
.then(result => {
console.log('最快的响应:', result)
})
.catch(error => {
console.error('所有请求都失败:', error)
})
// Promise.allSettled - 全部完成,不管成功失败
const settledPromises = [
fetchData('/api/success1'),
fetchData('/api/fail1'),
fetchData('/api/success2')
]
Promise.allSettled(settledPromises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`请求${index}成功:`, result.value)
} else {
console.log(`请求${index}失败:`, result.reason)
}
})
})
|
3. async/await时代#
基本async/await语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| async function fetchData() {
try {
const response = await fetch('/api/data')
const data = await response.json()
return data
} catch (error) {
console.error('获取数据失败:', error)
throw error
}
}
// 使用
fetchData()
.then(data => console.log(data))
.catch(error => console.error(error))
|
顺序执行异步操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| async function processUserData() {
const user = await fetchUser()
const posts = await fetchPosts(user.id)
const comments = await fetchComments(posts[0].id)
return { user, posts, comments }
}
// 并行执行异步操作
async function fetchAllData() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(1),
fetchComments(1)
])
return { user, posts, comments }
}
|
Promise深入理解#
1. Promise状态#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| const promise = new Promise((resolve, reject) => {
// pending -> fulfilled/rejected
})
// 状态检查
console.log(promise) // Promise { <pending> }
// 状态处理
promise
.then(value => {
console.log('fulfilled:', value) // fulfilled状态
})
.catch(error => {
console.log('rejected:', error) // rejected状态
})
.finally(() => {
console.log('settled:') // 无论成功失败都会执行
})
|
2. Promise错误处理#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| // 错误传播
function fetchWithRetry(url, maxRetries = 3) {
return new Promise((resolve, reject) => {
let retries = 0
const attempt = () => {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json()
})
.then(data => resolve(data))
.catch(error => {
retries++
if (retries <= maxRetries) {
console.log(`重试第${retries}次`)
setTimeout(attempt, 1000 * retries)
} else {
reject(error)
}
})
}
attempt()
})
}
// 错误处理最佳实践
function robustFetch(url, options = {}) {
return fetch(url, options)
.then(response => {
if (!response.ok) {
// 创建包含更多错误信息的Error对象
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
error.status = response.status
error.statusText = response.statusText
error.response = response
throw error
}
return response.json()
})
.catch(error => {
// 区分网络错误和其他错误
if (error.name === 'TypeError' || error.message.includes('Failed to fetch')) {
throw new Error('网络错误,请检查网络连接')
}
throw error
})
}
|
async/await最佳实践#
1. 错误处理策略#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| // 统一错误处理
class ApiError extends Error {
constructor(message, statusCode, data) {
super(message)
this.name = 'ApiError'
this.statusCode = statusCode
this.data = data
}
}
async function apiCall(url, options = {}) {
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`
},
...options
})
if (!response.ok) {
throw new ApiError(
`API请求失败: ${response.status} ${response.statusText}`,
response.status
)
}
return await response.json()
} catch (error) {
if (error instanceof ApiError) {
throw error // 重新抛出API错误
}
// 处理网络错误
if (error.name === 'TypeError') {
throw new ApiError('网络连接失败', 0)
}
throw error // 重新抛出其他错误
}
}
|
2. 并发控制#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| // 限制并发数量
class ConcurrencyLimiter {
constructor(limit = 3) {
this.limit = limit
this.running = 0
this.queue = []
}
async add(async fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject })
this.processQueue()
})
}
async processQueue() {
if (this.running >= this.limit || this.queue.length === 0) {
return
}
this.running++
const { fn, resolve, reject } = this.queue.shift()
try {
const result = await fn()
resolve(result)
} catch (error) {
reject(error)
} finally {
this.running--
this.processQueue()
}
}
}
// 使用示例
const limiter = new ConcurrencyLimiter(5)
async function fetchMultipleUrls(urls) {
const promises = urls.map(url =>
limiter.add(() => fetch(url).then(res => res.json())
)
return Promise.all(promises)
}
|
3. 超时控制#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // 超时包装器
function timeout(promise, delay) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('操作超时')), delay)
)
])
}
// 使用示例
async function fetchWithTimeout(url, delay = 5000) {
try {
const response = await timeout(
fetch(url),
delay
)
return response.json()
} catch (error) {
if (error.message === '操作超时') {
console.log(`请求${url}超时`)
}
throw error
}
}
|
实际应用场景#
1. 数据获取和缓存#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| class DataCache {
constructor(ttl = 300000) { // 5分钟
this.cache = new Map()
this.ttl = ttl
}
async get(key, fetchFn) {
const cached = this.cache.get(key)
if (cached && Date.now() - cached.timestamp < this.ttl) {
console.log('使用缓存数据:', key)
return cached.data
}
console.log('获取新数据:', key)
try {
const data = await fetchFn()
this.cache.set(key, {
data,
timestamp: Date.now()
})
return data
} catch (error) {
console.error('获取数据失败:', error)
throw error
}
}
}
// 使用示例
const cache = new DataCache()
async function getUserData(userId) {
return cache.get(`user-${userId}`, () =>
fetch(`/api/users/${userId}`).then(res => res.json())
)
}
|
2. 文件上传进度#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| async function uploadFile(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
const formData = new FormData()
formData.append('file', file)
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100)
onProgress(progress)
}
}
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(new Error(`上传失败: ${xhr.status}`))
}
}
xhr.onerror = () => reject(new Error('网络错误'))
xhr.open('POST', '/api/upload')
xhr.send(formData)
})
}
|
3. 轮询和实时数据#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| // 智能轮询
class SmartPoller {
constructor(fetchFn, options = {}) {
this.fetchFn = fetchFn
this.interval = options.interval || 5000
this.maxInterval = options.maxInterval || 30000
this.backoffFactor = options.backoffFactor || 1.5
this.maxRetries = options.maxRetries || 10
this.currentInterval = this.interval
this.retryCount = 0
this.timeoutId = null
}
start(callback) {
const poll = async () => {
try {
const result = await this.fetchFn()
this.retryCount = 0
this.currentInterval = this.interval
callback(null, result)
} catch (error) {
this.retryCount++
if (this.retryCount > this.maxRetries) {
callback(error, null)
return
}
this.currentInterval = Math.min(
this.currentInterval * this.backoffFactor,
this.maxInterval
)
console.log(`第${this.retryCount}次重试,${this.currentInterval}ms后重试`)
this.timeoutId = setTimeout(poll, this.currentInterval)
}
}
this.timeoutId = setTimeout(poll, 0)
}
stop() {
if (this.timeoutId) {
clearTimeout(this.timeoutId)
this.timeoutId = null
}
}
}
|
错误处理和调试#
1. 错误分类和处理#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| // 自定义错误类型
class NetworkError extends Error {
constructor(message, code) {
super(message)
this.name = 'NetworkError'
this.code = code
}
}
class ValidationError extends Error {
constructor(message, field) {
super(message)
this.name = 'ValidationError'
this.field = field
}
}
// 错误处理中间件
function errorHandler(error, req, res, next) {
if (error instanceof NetworkError) {
res.status(503).json({
error: '网络服务不可用',
code: error.code,
message: error.message
})
} else if (error instanceof ValidationError) {
res.status(400).json({
error: '验证失败',
field: error.field,
message: error.message
})
} else {
res.status(500).json({
error: '服务器内部错误',
message: error.message
})
}
}
|
2. 异步错误追踪#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // 错误追踪包装器
function withErrorTracking(fn) {
return async (...args) => {
try {
return await fn(...args)
} catch (error) {
// 记录错误上下文
console.error('异步操作失败:', {
error: error.message,
stack: error.stack,
args: args,
timestamp: new Date().toISOString()
})
// 发送错误到监控服务
trackError(error)
throw error
}
}
}
// 使用示例
const safeFetch = withErrorTracking(fetch)
async function fetchUserData(userId) {
const response = await safeFetch(`/api/users/${userId}`)
return response.json()
}
|
性能优化#
1. 避免不必要的Promise创建#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 不好的做法:每次调用都创建新的Promise
function getData() {
return new Promise((resolve) => {
resolve('data')
})
}
// 好的做法:缓存Promise或使用函数
const dataPromise = new Promise((resolve) => {
resolve('data')
})
function getData() {
return dataPromise
}
|
2. 合并异步操作#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // 不好的做法:串行执行
async function getDataSerial() {
const user = await fetchUser()
const posts = await fetchPosts(user.id)
const comments = await fetchComments(posts[0].id)
return { user, posts, comments }
}
// 好的做法:并行执行
async function getDataParallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(1),
fetchComments(1)
])
return { user, posts, comments }
}
// 部分并行执行
async function getDataMixed() {
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(1)
])
// 等posts加载完成后再获取comments
const comments = await fetchComments(posts[0].id)
return { user, posts, comments }
}
|
测试异步代码#
1. 测试Promise#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Jest测试Promise
test('Promise resolves with correct data', async () => {
const result = await fetchData()
expect(result).toBe('Hello, World!')
})
test('Promise rejects with error', async () => {
await expect(fetchData()).rejects.toThrow('Network error')
})
// 测试Promise.all
test('Promise.all resolves when all promises resolve', async () => {
const promises = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]
const result = await Promise.all(promises)
expect(result).toEqual([1, 2, 3])
})
|
2. 测试async/await#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Jest测试async函数
describe('User API', () => {
test('should fetch user data', async () => {
const userData = await fetchUser(1)
expect(userData).toHaveProperty('id')
expect(userData).toHaveProperty('name')
})
test('should handle network errors', async () => {
await expect(fetchUser(999)).rejects.toThrow('User not found')
})
})
// Mock测试异步操作
jest.mock('./api')
test('should call API with correct parameters', async () => {
const result = await fetchUser(1)
expect(api.fetchUser).toHaveBeenCalledWith(1)
expect(result).toEqual(mockUserData)
})
|
JavaScript异步编程的演进为我们提供了越来越优雅和强大的工具:
- 回调函数:基础但容易产生回调地狱
- Promise:解决了回调地狱,支持链式调用和错误处理
- async/await:让异步代码看起来像同步代码,更易读易维护
最佳实践要点:
- 优先使用async/await编写异步代码
- 合理使用Promise.all、Promise.race等并发控制方法
- 实现完善的错误处理和超时机制
- 注意性能优化,避免不必要的Promise创建
- 编写可测试的异步代码
掌握这些异步编程技巧,将帮助你构建更加健壮、高效的JavaScript应用。