JavaScript一篇文章学会使用Promise

142 阅读6分钟

Promise是JavaScript异步编程的基石,也是现代前端开发必须掌握的重要概念。本文将带你深入理解Promise,从基础使用到手写实现。

一、什么是Promise?

Promise是一种用于处理异步操作的对象,它代表一个尚未完成但预期将来会完成的操作。在Promise出现之前,JavaScript主要依赖回调函数来处理异步操作,但这容易导致"回调地狱"(Callback Hell)。

二、Promise的三种状态

Promise有三种状态:

  • pending:初始状态,既不是成功,也不是失败
  • fulfilled:操作成功完成
  • rejected:操作失败

状态一旦改变,就不会再变。

三、基本用法

1.创建Promise

const myPromise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('操作成功!');
    } else {
      reject('操作失败!');
    }
  }, 1000);
});

2.使用Promise

myPromise
  .then(result => {
    console.log(result); // "操作成功!"
  })
  .catch(error => {
    console.error(error); // "操作失败!"
  })
  .finally(() => {
    console.log('操作完成'); // 无论成功失败都会执行
  });

3.深入理解resolve和reject

resolvereject是Promise执行器函数(executor)的两个参数,它们的作用是:

  • resolve(value) :将Promise状态从pending改为fulfilled,并将value作为成功值传递给后续的.then()方法
  • reject(reason) :将Promise状态从pending改为rejected,并将reason作为失败原因传递给后续的.catch()方法

resolve的特点:

  • 可以接收任何类型的值
  • 如果传入另一个Promise,则会"跟随"这个Promise的状态
  • 如果传入thenable对象(具有then方法的对象),会采用相同的状态
// resolve可以接收各种值
Promise.resolve('直接成功') // 字符串
Promise.resolve({data: 'test'}) // 对象
Promise.resolve(Promise.reject('错误')) // 另一个Promise

// reject通常用于传递错误信息
Promise.reject(new Error('出错了'))
Promise.reject('错误信息')

四、Promise的链式调用

Promise的真正强大之处在于链式调用:

// 返回一个promise
function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ id: userId, name: '张三' });
    }, 500);
  });
}

// 返回一个promise
function fetchUserPosts(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(['文章1', '文章2', '文章3']);
    }, 500);
  });
}

fetchUserData(123)
  .then(user => {
    console.log('用户信息:', user);
    return fetchUserPosts(user.id);
  })
  .then(posts => {
    console.log('用户文章:', posts);
  })
  .catch(error => {
    console.error('发生错误:', error);
  });

五、Promise的静态方法

1.Promise.all()

等待所有Promise完成,如果有一个失败,整个Promise.all就会失败:

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3])
  .then(values => {
    console.log(values); // [3, 42, "foo"]
  });

2.Promise.race()

返回最先完成或拒绝的Promise:

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});
const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2])
  .then(value => {
    console.log(value); // "two"
  });

3.Promise.allSettled()

等待所有Promise完成(无论成功或失败):

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => {
  reject('失败');
});

Promise.allSettled([promise1, promise2])
  .then(results => {
    console.log(results);
    // [
    //   { status: "fulfilled", value: 3 },
    //   { status: "rejected", reason: "失败" }
    // ]
  });

六、async/await:更优雅的异步编程

1. async函数

async函数是ES2017引入的语法糖,让异步代码看起来像同步代码:

async function fetchData() {
  try {
    const user = await fetchUserData(123);
    const posts = await fetchUserPosts(user.id);
    console.log('用户信息:', user);
    console.log('用户文章:', posts);
    return { user, posts };
  } catch (error) {
    console.error('发生错误:', error);
  }
}

// async函数总是返回Promise
fetchData().then(result => {
  console.log('最终结果:', result);
});

2. await表达式

await关键字只能在async函数内部使用,它会暂停async函数的执行,等待Promise解决:

// await会"解开"Promise的值
async function example() {
  const value = await Promise.resolve(42); // value = 42
  const error = await Promise.reject('出错了').catch(err => err); // 需要捕获错误
  
  // 并行执行多个异步操作
  const [user, settings] = await Promise.all([
    fetchUserData(123),
    fetchUserSettings(123)
  ]);
}

3. 错误处理

async/await可以使用传统的try-catch语法:

async function safeFetch() {
  try {
    const response = await fetch('api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('请求失败:', error);
    // 可以返回默认值或重新抛出错误
    return { default: 'value' };
    // 或:throw new Error('处理后的错误');
  }
}

4. async/await的优势

  • 代码更简洁:避免了回调地狱和冗长的链式调用

  • 错误处理更直观:可以使用try-catch块

  • 调试更方便:异步代码的调用栈更清晰

  • 逻辑更清晰:异步操作可以像同步代码一样编写

七、手写实现Promise

理解Promise的最佳方式就是自己实现一个简化版:

class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      } else if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      } else if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        });

        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        });
      }
    });

    return promise2;
  }

  resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected for promise'));
    }

    if (x instanceof MyPromise) {
      x.then(resolve, reject);
    } else {
      resolve(x);
    }
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  finally(callback) {
    return this.then(
      value => MyPromise.resolve(callback()).then(() => value),
      reason => MyPromise.resolve(callback()).then(() => { throw reason })
    );
  }

  static resolve(value) {
    return new MyPromise(resolve => resolve(value));
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
}

八、实际应用场景

1. 异步请求封装

// 使用async/await封装
async function request(url, options = {}) {
  try {
    const response = await fetch(url, options);
    if (!response.ok) {
      throw new Error(`HTTP错误: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('请求错误:', error);
    throw error; // 重新抛出以便外部处理
  }
}

// 使用示例
async function loadUserData() {
  try {
    const user = await request('/api/user');
    const posts = await request(`/api/user/${user.id}/posts`);
    return { user, posts };
  } catch (error) {
    // 统一错误处理
    showErrorMessage('数据加载失败');
  }
}

2. 图片加载

async function loadImages(imageUrls) {
  const loadImage = (src) => {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = reject;
      img.src = src;
    });
  };

  try {
    // 并行加载所有图片
    const imagePromises = imageUrls.map(url => loadImage(url));
    const images = await Promise.all(imagePromises);
    
    images.forEach(img => {
      document.body.appendChild(img);
    });
    
    return images;
  } catch (error) {
    console.error('图片加载失败:', error);
  }
}

九、常见陷阱与最佳实践

1. 避免Promise嵌套

// 错误示范
getUser().then(user => {
  getPosts(user.id).then(posts => {
    // 嵌套过深
  });
});

// 正确示范 - 使用async/await
async function getUserData() {
  const user = await getUser();
  const posts = await getPosts(user.id);
  return { user, posts };
}

// 或使用链式调用
getUser()
  .then(user => getPosts(user.id))
  .then(posts => {
    // 扁平结构
  });

2. 总是处理错误

// 不要这样做
somePromise.then(result => {
  // 忘记处理错误
});

// 应该这样做 - 使用async/await
async function safeOperation() {
  try {
    const result = await somePromise;
    // 处理成功情况
  } catch (error) {
    // 处理错误
  }
}

// 或使用catch
somePromise
  .then(result => {
    // 处理成功情况
  })
  .catch(error => {
    // 处理错误
  });

3. 合理使用并行执行

// 顺序执行(慢)
async function sequential() {
  const a = await task1(); // 等待1秒
  const b = await task2(); // 再等待1秒,总共2秒
  return { a, b };
}

// 并行执行(快)
async function parallel() {
  const [a, b] = await Promise.all([task1(), task2()]); // 只等待1秒
  return { a, b };
}

总结

Promise是现代JavaScript异步编程的核心,它通过清晰的链式调用和错误处理机制,极大地改善了代码的可读性和可维护性。而async/await语法在Promise的基础上提供了更直观、更接近同步代码的编程体验。 关键要点:

  • Promise的三种状态(pending、fulfilled、rejected)和不可变性
  • resolve/reject的作用和用法
  • 链式调用的优势和实现原理
  • async/await让异步代码更易读易写
  • 合理的错误处理是健壮代码的保障

掌握Promise和async/await不仅有助于编写更好的异步代码,也是理解现代JavaScript生态的基础。在实际开发中,根据场景选择合适的模式:简单的链式操作可用Promise链,复杂的异步逻辑推荐使用async/await。多练习、多思考,你会越来越熟练地运用这些强大的异步编程工具!