11.1.3 期约连锁与期约合成

多个期约组合在一起可以构成强大的代码逻辑。这种组合可以通过两种方式实现:期约连锁与期约合成。前者就是一个期约接一个期约的拼接,后者则是将多个期约组合为一个期约。

1、期约连锁

每个期约实例的方法(then()、catch()和finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的“期约连锁”。

// 基础示例
fetch('/api/users')
  .then(response => response.json())
  .then(users => {
    console.log(users);
    return fetch(`/api/users/${users[0].id}`);
  })
  .then(response => response.json())
  .then(userDetail => {
    console.log(userDetail);
  })
  .catch(error => {
    console.error('请求失败:', error);
  });

以上这种方式使得每个后续的处理程序都会等待前一个期约解决,然后实例化一个新期约并返回它。这种结构可以简洁地将异步任务串行化。

2. 期约合成(Promise Composition)
(1)Promise.all() - 传统方式

该静态方法创建的期约会在一组期约全部解决之后再解决,接收一个可迭代对象,返回一个新期约。可迭代对象中的元素会 通过Promise.resolve()转换为期约。

// 等待所有期约完成
const promise1 = fetch('/api/users');
const promise2 = fetch('/api/posts');
const promise3 = fetch('/api/comments');

Promise.all([promise1, promise2, promise3])
  .then(responses => {
    // responses是一个包含所有结果的数组
    return Promise.all(responses.map(r => r.json()));
  })
  .then(([users, posts, comments]) => {
    console.log('所有数据:', { users, posts, comments });
  })
  .catch(error => {
    // 任何一个期约被拒绝都会进入这里
    console.error('某个请求失败:', error);
  });

如果至少有一个包含的期约待定,则合成的期约也会待定;如果有一个包含的期约拒绝,则合成的期约也会拒绝;如果所有期约都成功解决,则合成的期约解决值就是所有包含期约解决值的数组,按照迭代器顺序。如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由,之后的不影响最终的拒绝理由。不过这并不影响所有包含期约正常的拒绝操作。

(2)Promise.allSettled()

ES2020引入的Promise.allSettled()提供了更精细的控制:

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

Promise.allSettled(promises)
  .then(results => {
    const successful = results.filter(r => r.status === 'fulfilled');
    const failed = results.filter(r => r.status === 'rejected');
    
    console.log(`成功: ${successful.length}, 失败: ${failed.length}`);
    
    // 只处理成功的请求
    return Promise.all(
      successful.map(r => r.value.json())
    );
  })
  .then(data => {
    console.log('成功的数据:', data);
  });
(3)Promise.race() - 竞速模式

这个静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。接收一个可迭代对象,返回一个新期约。无论是解决还是拒绝,只要是第一个落定的期约,Promise.race() 就会包装其解决值或拒绝理由并返回新期约。

// 获取最快响应的数据源
const timeoutPromise = new Promise((_, reject) => {
  setTimeout(() => reject(new Error('请求超时')), 5000);
});

const dataPromise = fetch('/api/data');

Promise.race([dataPromise, timeoutPromise])
  .then(response => {
    console.log('数据获取成功');
    return response.json();
  })
  .catch(error => {
    console.error('请求超时或失败:', error);
  });
(4)Promise.any() - ES2021新特性
// 等待第一个成功的期约
const primaryAPI = fetch('/api/primary/data');
const backupAPI = fetch('/api/backup/data');
const cacheAPI = fetch('/api/cache/data');

Promise.any([primaryAPI, backupAPI, cacheAPI])
  .then(firstResponse => {
    console.log('从最快可用的数据源获取数据');
    return firstResponse.json();
  })
  .then(data => {
    console.log('数据:', data);
  })
  .catch(error => {
    // 所有期约都失败时才会进入这里
    console.error('所有数据源都不可用:', error);
  });
3、串行期约合成

后续期约使用之前期约的返回值来串联期约,这很像函数合成,即将多个函数合并成一个函数:

function addTwo(x) { return x + 2; }
function addThree(x) { return x + 3; }
function addFive(x) { return x + 5; }

function compose(...fns){
  return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}
let addTen = compose(addTwo, addThree, addFive);
addTen(8).then(console.log);	//18
4. 高级期约模式
动态期约连锁
class AsyncPipeline {
  constructor(initialValue) {
    this.promise = Promise.resolve(initialValue);
  }

  then(fn) {
    this.promise = this.promise.then(fn);
    return this;
  }

  catch(fn) {
    this.promise = this.promise.catch(fn);
    return this;
  }

  finally(fn) {
    this.promise = this.promise.finally(fn);
    return this;
  }

  get() {
    return this.promise;
  }
}

// 使用示例
new AsyncPipeline(10)
  .then(x => x * 2)
  .then(x => fetch(`/api/process/${x}`))
  .then(response => response.json())
  .then(data => data.result)
  .catch(error => {
    console.error('处理失败:', error);
    return { result: 0 }; // 默认值
  })
  .get()
  .then(finalResult => {
    console.log('最终结果:', finalResult);
  });
带缓存的期约合成
class PromiseWithCache {
  constructor() {
    this.cache = new Map();
  }

  getOrCreate(key, promiseFactory) {
    if (this.cache.has(key)) {
      return this.cache.get(key);
    }

    const promise = promiseFactory()
      .then(result => {
        // 缓存成功结果
        this.cache.set(key, Promise.resolve(result));
        return result;
      })
      .catch(error => {
        // 从缓存中移除失败的期约
        this.cache.delete(key);
        throw error;
      });

    this.cache.set(key, promise);
    return promise;
  }
}

// 使用示例
const cache = new PromiseWithCache();

async function getUserData(userId) {
  return cache.getOrCreate(`user-${userId}`, () => 
    fetch(`/api/users/${userId}`).then(r => r.json())
                          );
}
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐