Skip to content
imshengli blog
Go back

ES6 Promise:从链式调用到手写实现

Promise 从入门到深入:链式调用、all/race/allSettled、错误处理最佳实践,附手写简易 Promise。

· 12 min

总结

Promise 是一种异步解决方案, 在异步流程中,如果想在未来某一时刻执行某项操作, 可以通过回调或事件的方式处理, 但存在嵌套过深的场景。

通过Promise,可以通过链式链路进行操作, 简化了异步操作的控制; ES6 提供了原生的解决方案。

如果自己完成一个Promise解决方案, 应该如何去处理呢?

简介

异步编程的一种解决方案。 ES6统一了用法,原生提供了Promise对象。

特点:

基本用法

// Promise对象是一个构造函数,用来生成Promise实例。
const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
  // 会继续执行这行代码
  // 但最好不要这样做,在 then 里执行就好了;
  console.log(2);
});
// then 接受两个回调函数作为参数:
// 分别指定resolved状态和rejected状态的回调函数。
// 第二个方法可选
promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

异步加载图片

function loadImageAsync(url) {
  // 返回一个 Promise 实例
  return new Promise(function(resolve, reject) {
    const image = new Image();
    image.onload = function() {
      resolve(image);
    };
    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };
    image.src = url;
  });
}

Promise.prototype.then()

添加状态改变时的回调函数。

then方法返回的是一个新的Promise实例

如果返回的是一个 Promise 实例, 会等待该Promise对象的状态发生变化,才会被调用。

loadImageAsync('https://imshengli.com/images/logo.png')
.then(image => {
  console.log(image);
  return image;
}).then(res => {
  console.log(res);
});

Promise.prototype.catch()

.then(null, rejection)的别名, 用于指定发生错误时的回调函数, 返回的还是 Promise 对象。

then方法指定的回调函数,如果运行中抛出错误, 也会被catch方法捕获。

Promise 对象的错误具有“冒泡”性质, 会一直向后传递,直到被捕获为止。

建议总是使用catch方法,而不使用then方法的第二个参数。

跟传统的try/catch代码块不同的是, 如果没有使用catch方法指定错误处理的回调函数, Promise 对象内部抛出的错误不会传递到外层代码, 即不会有任何反应。

// 不会退出进程、终止脚本执行,
const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};
someAsyncThing().then(function() {
  console.log('everything is great');
});
setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

Promise.all()

将多个 Promise 实例,包装成一个新的 Promise 实例。

// 参数可以不是数组,但必须具有 Iterator 接口,
// 且返回的每个成员都是 Promise 实例;
// 如果不是,就会先调用的Promise.resolve方法,
// 将参数转为 Promise 实例。
const p = Promise.all([p1, p2, p3]);
// 都变成fulfilled,p的状态才会变成fulfilled,
// 有一个被rejected,p的状态就变成rejected。

// 如果 作为参数的 Promise 实例,
// 自己定义了catch方法,那么它一旦被rejected,
// 并不会触发Promise.all()的catch方法。

Promise.race()

将多个 Promise 实例,包装成一个新的 Promise 实例。

// 只要p1、p2、p3之中有一个实例率先改变状态,
// p的状态就跟着改变。
// 那个率先改变的 Promise 实例的返回值,
// 就传递给p的回调函数。
const p = Promise.race([p1, p2, p3]);

Promise.resolve()

将现有对象转为 Promise 对象。

// 将 jQuery 生成的deferred对象,转为一个新的 Promise 对象。
const jsPromise = Promise.resolve($.ajax('/whatever.json'));

参数:

注意:

立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时, 而不是在下一轮“事件循环”的开始时。 setTimeout(fn, 0)在下一轮“事件循环”开始时执行。

Promise.reject()

返回一个新的 Promise 实例,该实例的状态为rejected。

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
  console.log(s)
});
// 出错了

Promise.reject()方法的参数, 会原封不动地作为reject的理由,变成后续方法的参数。

const thenable = {
  then(resolve, reject) {
    reject('出错了');
  }
};

Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true

done()

自行添加: 总是处于回调链的尾端,保证抛出任何可能出现的错误。

实现:

Promise.prototype.done = function (onFulfilled, onRejected) {
  this.then(onFulfilled, onRejected)
    .catch(function (reason) {
      // 抛出一个全局错误
      setTimeout(() => { throw reason }, 0);
    });
};

finally()

自行添加: 不管 Promise 对象最后状态如何,都会执行的操作。

实现:

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

Promise.try()

自行添加: 不知道或者不想区分,函数f是同步函数还是异步操作, 但是想用 Promise 来处理它。

// 缺点,就是如果f是同步函数,
// 那么它会在本轮事件循环的末尾执行。
Promise.resolve().then(f);

改进:

const f = () => console.log('now');
(
  () => new Promise(
    resolve => resolve(f())
  )
)();
console.log('next');
// now
// next

Promise.allSettled()

ES2020 引入。等待所有 Promise 都完成(无论 fulfilled 还是 rejected),返回每个结果的状态。

Promise.all 的区别:all 遇到第一个 reject 就短路,allSettled 永远等所有都结束。

const promises = [
  fetch('/api/user'),
  fetch('/api/posts'),
  fetch('/api/invalid-url')  // 这个会失败
];

Promise.allSettled(promises).then(results => {
  // results 的每一项都有 status 字段
  results.forEach(result => {
    if (result.status === 'fulfilled') {
      console.log('成功:', result.value);
    } else {
      console.log('失败:', result.reason);
    }
  });
});

// 适用场景:并发请求多个接口,不希望一个失败影响其他结果

Promise.any()

ES2021 引入。只要有一个 fulfilled 就返回该结果;全部 rejected 才抛出 AggregateError。

const promises = [
  fetch('https://cdn1.example.com/data.json'),
  fetch('https://cdn2.example.com/data.json'),
  fetch('https://cdn3.example.com/data.json')
];

// 最快成功的那个 CDN 的结果
Promise.any(promises)
  .then(response => console.log('最快成功:', response))
  .catch(err => {
    // 只有全部失败才走这里
    console.log(err instanceof AggregateError); // true
    console.log(err.errors); // 所有错误的数组
  });

Promise.all / race / allSettled / any 对比:

方法短路条件返回值
all任意一个 reject所有 fulfilled 值的数组
race任意一个 settle第一个 settle 的值
allSettled不短路所有结果的状态数组
any任意一个 fulfill第一个 fulfilled 值

错误处理最佳实践

// 1. 始终在链的末尾使用 catch
fetchData()
  .then(process)
  .then(save)
  .catch(err => {
    // 统一处理以上任意步骤的错误
    console.error('Pipeline failed:', err);
  });

// 2. async/await 配合 try-catch(推荐写法)
async function loadUser(id) {
  try {
    const response = await fetch(`/api/user/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (err) {
    console.error('加载用户失败:', err);
    throw err; // 向上层传播,或返回默认值
  }
}

// 3. 避免在 then 里嵌套 Promise(反模式)
// 错误写法
fetchUser().then(user => {
  fetchPosts(user.id).then(posts => {
    // 嵌套地狱,又回去了
  });
});

// 正确写法:利用链式调用
fetchUser()
  .then(user => fetchPosts(user.id))
  .then(posts => renderPosts(posts))
  .catch(handleError);

手写简易 Promise

理解 Promise 的核心机制:状态管理 + 回调队列 + 异步执行。

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

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

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

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

  then(onFulfilled, onRejected) {
    // 参数透传
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e; };

    const promise2 = new MyPromise((resolve, reject) => {
      const handleFulfilled = () => {
        // 异步执行,模拟微任务
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            // 简化处理:如果返回的是 Promise 则等待它
            if (x instanceof MyPromise) {
              x.then(resolve, reject);
            } else {
              resolve(x);
            }
          } catch (err) {
            reject(err);
          }
        }, 0);
      };

      const handleRejected = () => {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            if (x instanceof MyPromise) {
              x.then(resolve, reject);
            } else {
              resolve(x);
            }
          } catch (err) {
            reject(err);
          }
        }, 0);
      };

      if (this.status === 'fulfilled') {
        handleFulfilled();
      } else if (this.status === 'rejected') {
        handleRejected();
      } else {
        // pending 状态,先存储回调
        this.onFulfilledCallbacks.push(handleFulfilled);
        this.onRejectedCallbacks.push(handleRejected);
      }
    });

    return promise2;
  }

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

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

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

// 验证
new MyPromise((resolve) => {
  setTimeout(() => resolve('done'), 100);
})
.then(val => {
  console.log(val); // 'done'
  return 'next';
})
.then(val => console.log(val)); // 'next'

常见陷阱

1. 忘记 return

// 错误:then 里没有 return,下一个 then 收到 undefined
fetchUser()
  .then(user => {
    fetchPosts(user.id); // 忘记 return
  })
  .then(posts => {
    console.log(posts); // undefined!
  });

// 正确
fetchUser()
  .then(user => {
    return fetchPosts(user.id);
  })
  .then(posts => {
    console.log(posts); // 正确拿到 posts
  });

2. 在 catch 后面的 then 仍会执行

Promise.reject('error')
  .catch(err => {
    console.log('caught:', err);
    // catch 返回的是 fulfilled 状态的 Promise
  })
  .then(() => {
    // 这里会执行!因为上面 catch 正常返回了
    console.log('继续执行');
  });

3. Promise 构造函数是同步执行的

console.log('1');
new Promise((resolve) => {
  console.log('2'); // 同步执行
  resolve();
}).then(() => {
  console.log('3'); // 微任务
});
console.log('4');
// 输出顺序:1 2 4 3

4. 循环引用导致死锁

const p = new Promise((resolve) => {
  resolve(100);
});
const p2 = p.then(() => {
  return p2; // TypeError: Chaining cycle detected
});

5. 并发控制

当需要限制并发数量时,不能直接用 Promise.all

// 限制最大并发数的工具函数
async function asyncPool(limit, items, fn) {
  const results = [];
  const executing = [];

  for (const item of items) {
    const p = fn(item);
    results.push(p);

    if (items.length >= limit) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }

  return Promise.all(results);
}

// 使用:最多同时 3 个请求
const urls = ['/api/1', '/api/2', '/api/3', '/api/4', '/api/5'];
asyncPool(3, urls, url => fetch(url));

参考文档


Share this post on:

Previous Post
JavaScript 字符串:常用方法、模板字符串与 Unicode
Next Post
JavaScript 正则表达式:语法、断言与常用模式