Skip to content

JavaScript 进阶 八:异步编程

约 1202 字大约 4 分钟

javascript

2020-02-08

在现代前端开发中,异步编程已成为不可或缺的核心技能。无论是处理用户交互、网络请求还是文件操作,异步编程都能帮助我们构建更流畅、响应更快的应用。本文将深入探讨 JavaScript 异步编程的演进历程、核心概念和最佳实践。

异步编程的必要性

JavaScript 作为一门单线程语言,如果所有操作都同步执行,遇到耗时任务时就会阻塞整个线程,导致页面卡顿甚至无响应。异步编程通过非阻塞的方式处理这些任务,让主线程能够继续响应其他操作。

现实类比

想象你在餐厅点餐:同步方式就像你站在柜台前等待厨师做好每一道菜才点下一道;异步方式则是你点完所有菜后找个座位等待,期间可以玩手机、聊天,服务员会在菜准备好时通知你。

异步编程的演进历程

1. 回调函数(Callback)

回调函数是最基础的异步处理方式:

回调函数示例
function fetchData(callback) {
  setTimeout(() => {
    const data = { name: 'John', age: 30 }
    callback(null, data)
  }, 1000)
}

fetchData((error, result) => {
  if (error) {
    console.error('Error:', error)
  }
  else {
    console.log('Data:', result)
  }
})

回调地狱

当多个异步操作需要顺序执行时,回调函数容易形成"金字塔"结构:

getUser(userId, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      // 更多嵌套...
    })
  })
})

2. Promise

ES6 引入的 Promise 提供了更优雅的异步处理方式:

Promise 基础使用
function fetchUser(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId) {
        resolve({ id: userId, name: 'Alice' })
      }
      else {
        reject(new Error('User ID is required'))
      }
    }, 1000)
  })
}

fetchUser(123)
  .then((user) => {
    console.log('User:', user)
    return user.name
  })
  .then((name) => {
    console.log('Name:', name)
  })
  .catch((error) => {
    console.error('Error:', error)
  })
Promise.all
// 并行执行多个异步操作
Promise.all([
  fetchUser(1),
  fetchUser(2),
  fetchUser(3)
]).then((users) => {
  console.log('All users:', users)
}).catch((error) => {
  console.error('One request failed:', error)
})

3. Async/Await

ES2017 引入的 async/await 让异步代码看起来像同步代码:

Async/Await 使用
async function getUserData(userId) {
  try {
    const user = await fetchUser(userId)
    const posts = await fetchUserPosts(user.id)
    const comments = await fetchPostComments(posts[0].id)

    return {
      user,
      posts,
      comments
    }
  }
  catch (error) {
    console.error('Failed to fetch user data:', error)
    throw error
  }
}

// 使用
getUserData(123)
  .then(data => console.log('User data:', data))
  .catch(error => console.error('Error:', error))

异步编程的核心概念

1. 事件循环(Event Loop)

JavaScript 的事件循环机制是异步编程的基石:

事件循环示例
console.log('Start')

setTimeout(() => {
  console.log('Timeout 1')
}, 0)

Promise.resolve().then(() => {
  console.log('Promise 1')
})

console.log('End')

// 输出顺序:
// Start
// End
// Promise 1
// Timeout 1

执行顺序

  • 同步任务立即执行
  • 微任务(Promise)在当前任务结束后立即执行
  • 宏任务(setTimeout)在下一个事件循环中执行

2. 错误处理

  • Promise 错误处理

    fetchUser(null)
      .then(user => console.log(user))
      .catch(error => console.error('Caught:', error))
  • Async/Await 错误处理

    async function safeFetch() {
      try {
        const user = await fetchUser(null)
        return user
      }
      catch (error) {
        console.error('Fetch failed:', error)
        return null
      }
    }
  • 全局错误处理

    window.addEventListener('unhandledrejection', (event) => {
      console.warn('Unhandled promise rejection:', event.reason)
      event.preventDefault()
    })

实际应用场景

1. 并发请求优化

并发请求模式
async function fetchDashboardData() {
  const [user, notifications, messages] = await Promise.all([
    fetchUser(currentUserId),
    fetchNotifications(),
    fetchMessages()
  ])

  return { user, notifications, messages }
}

// 带错误处理的并发
async function fetchWithFallback() {
  try {
    const data = await Promise.allSettled([
      fetchFromPrimary(),
      fetchFromBackup()
    ])

    const successful = data.find(result => result.status === 'fulfilled')
    return successful?.value || null
  }
  catch (error) {
    console.error('All requests failed')
    return null
  }
}

2. 请求取消

可取消的异步操作
class CancelableFetch {
  constructor() {
    this.controller = new AbortController()
  }

  async fetch(url) {
    try {
      const response = await fetch(url, {
        signal: this.controller.signal
      })
      return await response.json()
    }
    catch (error) {
      if (error.name === 'AbortError') {
        console.log('Request was canceled')
      }
      throw error
    }
  }

  cancel() {
    this.controller.abort()
  }
}

// 使用
const fetchInstance = new CancelableFetch()
fetchInstance.fetch('/api/data')
  .then(data => console.log(data))
  .catch(error => console.error(error))

// 需要时取消
// fetchInstance.cancel();

性能优化技巧

1. 防抖与节流

防抖
function debounce(func, wait) {
  let timeout
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout)
      func(...args)
    }
    clearTimeout(timeout)
    timeout = setTimeout(later, wait)
  }
}

// 搜索框使用
const search = debounce((query) => {
  fetchResults(query)
}, 300)

2. 懒加载与预加载

图片懒加载
class LazyLoader {
  constructor() {
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          this.loadImage(entry.target)
          this.observer.unobserve(entry.target)
        }
      })
    })
  }

  observe(element) {
    this.observer.observe(element)
  }

  loadImage(img) {
    const src = img.getAttribute('data-src')
    if (src) {
      img.src = src
      img.removeAttribute('data-src')
    }
  }
}

最佳实践总结

关键要点

  1. 优先使用 async/await:代码更清晰,错误处理更简单
  2. 合理处理错误:不要忽略 Promise 拒绝,使用 try-catch 或 .catch()
  3. 避免过度嵌套:使用 Promise.all 处理并行任务
  4. 考虑性能影响:适当使用防抖、节流和懒加载
  5. 提供加载状态:给用户明确的反馈
  6. 测试异步代码:确保在各种场景下都能正常工作

参考