前端-Promise详解与实例

Promise.all和Promise.race传入空数组

关于将 Promise.all和 Promise.race传入空数组的两段代码的输出结果说法正确的是:

Promise.all([]).then(res => {
  console.log('all', res); // 输出all []
})

Promise.race([]).then(res => {
  console.log('race', res); // 没有任何输出
})

Promise.all([])会返回一个成功状态的promise

Promise.race([])会返回一个pending状态的promise

promise的超时控制

Promise原生是不支持设置超时时间的,也不支持中断请求。如果我们想要实现超时控制或者取消重复请求这样的需求,只能寻求其它思路,另外这两个需求本质上都是中断请求,并且我们可以随

控制什么时候中断。

总体思路是:创建一个新的newPromise,和原来用于发送请求的originPromise作为参数传入Promise.race方法中,接下来使用这个race方法返回的promise,上边添加then和catch方法。如果

要中断原来的promise,只需要将newPromise变成失败状态即可。

需求1:自定义实现超时控制

请求是无法设置超时时间的,因此我们需要自己去模拟一个超时控制。

方法一:使用promise.race

// 封装一个延时失败的promise
function sleep(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('timeout')
    }, time);
  })
}

// 第一个参数是正常的promise,第二个参数设置超时时间
function timeoutPromise(promise, time) {
  return Promise.race([promise, sleep(time)]);
}

// 测试
function createPromise(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('done');
    }, time);
  })
}

// 超时。设置正常的promise在2s后成功,超时时间是1s
timeoutPromise(createPromise(2000), 1000).then(res => {
  console.log(res);
}).catch(err => {
  console.log(err); // timeout
});

// 不超时
timeoutPromise(createPromise(2000), 3000).then(res => {
  console.log(res); // done
}).catch(err => {
  console.log(err);
});

方法二:不使用promise.race。但是本质上相当于自定义实现了promise.race,手动创建一个promise的数组,让每一个promise都开始执行,谁的状态先改变就以谁的状态为准

function sleep(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('timeout')
    }, time);
  });
}

function timeoutPromise(promise, time) {
  const promiseArr = [promise, sleep(time)];
  
  return new Promise((resolve, reject) => {
    promiseArr.forEach(p => {
      p.then(res => {
        resolve(res);
      }).catch(err => {
        reject(err);
      })
    })
  })
}

// 测试
function createPromise(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('done');
    }, time);
  })
}

// 超时
timeoutPromise(createPromise(2000), 1000).then(res => {
  console.log(res);
}).catch(err => {
  console.log(err); // timeout
});

// 不超时
timeoutPromise(createPromise(2000), 3000).then(res => {
  console.log(res); // done
}).catch(err => {
  console.log(err);
});

需求2:转盘问题

转盘问题,一个抽奖转盘动画效果有5秒,但是一般来说向后端请求转盘结果只需要不到一秒,因此请求结果至少得等5秒才能展现给用户。

需要考虑两种情况。

  1. 转盘动画还未完成,请求结果已经拿到了,此时要等到动画完成再展示结果给用户。
  2. 转盘动画完成了,请求结果还未拿到,此时需要等待结果返回(可以设置请求超时时间)。

所以,转盘问题更适合用Promise.all()来解决。

// 转盘动画在time秒后完成
function plate(time) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('我转完了~');
    }, time);
  })
}

// 第一个参数是包裹着请求的promise,第二个参数是转盘动画时间
function wrapPromise(promise, time) {
  return Promise.all([promise, plate(time)]);
}

// 测试
function createPromise(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('done');
    }, time);
  })
}

wrapPromise(createPromise(2000), 3000).then(res => {
  console.log(res); // ["done", "我转完了~"]
});

wrapPromise(createPromise(2000), 1000).then(res => {
  console.log(res); //  ["done", "我转完了~"]
});

取消重复请求

同一类请求是有序发出的(根据按钮点击的次序),但是响应顺序却是无法预测的,我们通常只希望渲染最后一次发出请求响应的数据,而其他数据则丢弃。因此,我们需要丢弃(或不处理)除最

一次请求外的其他请求的响应数据。

function CancelablePromise() {
  this.pendingPromise = null;
}

// 参数是一个正常的包裹着请求的promise
CancelablePromise.prototype.request = function (promise) {
  if (this.pendingPromise) {
    this.cancel('取消重复请求');
  }

  const _promise = new Promise((resolve, reject) => {
    this.reject = reject;
  });

  const newPromise = Promise.race([promise, _promise]);
  this.pendingPromise = newPromise;
  return newPromise;
}

CancelablePromise.prototype.cancel = function (reason) {
  this.reject(new Error(reason));
  this.pendingPromise = null;
}

// 测试
function createRequest(delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('请求完成')
    }, delay);
  })
}

const cancelablePromise = new CancelablePromise();
for (let i = 0; i < 3; i++) {
  cancelablePromise.request(createRequest(1000)).then(res => {
    console.log(res);
  }).catch(err => {
    console.error(err);
  })
}

限制并发请求数

Promise.allSettled不适合应对这样的场景,它能控制的粒度还是太粗了。它必须等待所有Promise都resolve或reject后才能继续。比如现在总共有10个请求,并发请求数限制为4个,当其中

个请求完成时,应该就可以进行下一个请求了,为了最高效率,要始终保证并发请求数量被最大程度使用。但是如果使用Promise.allSettled,它必须等待4个请求都完成了才能进行接下来的4个

求,这显然是不合理的。

function limitRequest(requestFnArr, limit) {
  function request(requestFn) {
    requestFn().finally(() => {
      // 如果前边的请求进行完成了,就可以处理新的请求
      if (_requestFnArr.length > 0) {
        request(_requestFnArr.shift());
      }
    })
  }

  // 限制并发量
  const _requestFnArr = [...requestFnArr];
  for (let i = 0; i < limit && _requestFnArr.length > 0; i++) {
    request(_requestFnArr.shift());
  }
}

// 测试
function createRequest(delay) {
  return function () {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('done')
      }, delay);
    }).then(res => 
      console.log(res);
    })
  }
}

const requestFns = [];
for (let i = 0; i < 10; i++) {
  requestFns.push(createRequest(1000));
}

limitRequest(requestFns, 4);

实现有并行限制的Promise调度器

class Scheduler {
  constructor(limit) {
    this.limit = limit;
    this.count = 0;
    this.taskList = [];
  }

  run(promiseCreator) {
    promiseCreator().finally(() => {
      this.count--;
      if (this.taskList.length > 0) {
        this.run(this.taskList.shift());
      }
    })
  }

  add(promiseCreator) {
    this.taskList.push(promiseCreator);
    if (this.count < this.limit) {
      this.run(this.taskList.shift());
      this.count++;
    }
  }
}

// 测试
const scheduler = new Scheduler(3);
const timeout = time => new Promise(resolve => {
  setTimeout(resolve, time);
});
const addTask = (time, value) => {
  scheduler.add(() => {
    return timeout(time).then(() => {
      console.log(value);
    })
  })
};

addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');

自定义实现allSettled

function allSettled(promises) {
  let result = [];
  let count = 0;
  return new Promise(resolve => {
    for (let i = 0; i < promises.length; i++) {
      let promise = Promise.resolve(promises[i]);
      let obj = {};
      promise.then(res => {
        obj = {
          status: 'fulfilled',
          value: res
        }
      }).catch(err => {
        obj = {
          status: 'rejected',
          reason: err
        }
      }).finally(() => {
        // 注意这里需要根据下标来存值,否则无法保证返回值数组与参数顺序一致
        result[i] = obj;
        if (++count === promises.length) {
          resolve(result);
        }
      })
    }
  })
}

自定义实现Promise.all和Promise.race

Promise.all:

function promiseAll(promises) {
    // 传入的参数不一定是数组对象,可以是"遍历器"
    // 如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。
    promises = Array.from(promises);
    let count = 0;
    let newValues = [];

    return new Promise(((resolve, reject) => {

      for (let i = 0; i < promises.length; i++) {
        Promise.resolve(promises[i])
            .then(res => {
              // 保存这个promise实例的value
          		// 注意这里需要根据下标来存值,否则无法保证返回值数组与参数顺序一致
              result[i] = res;

              // 通过计数器,标记是否所有实例均 fulfilled
              if (++count === promises.length) {
                resolve(newValues);
              }
            })
            .catch(err => {
              reject(err);
            })
      }
    }))
  }function promiseAll(promises) {
    // 传入的参数不一定是数组对象,可以是"遍历器"
    // 如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。
    promises = Array.from(promises);
    let count = 0;
    let newValues = [];

    return new Promise(((resolve, reject) => {

      for (let i = 0; i < promises.length; i++) {
        Promise.resolve(promises[i])
            .then(res => {
              // 保存这个promise实例的value
          		// 注意这里需要根据下标来存值,否则无法保证返回值数组与参数顺序一致
              result[i] = res;

              // 通过计数器,标记是否所有实例均 fulfilled
              if (++count === promises.length) {
                resolve(newValues);
              }
            })
            .catch(err => {
              reject(err);
            })
      }
    }))
  }

Promise.race:

function race(promises) {
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promises[i]; i++) {
      Promise.resolve(promises[i]).then(res => {
        resolve(res);
      }).catch(err => {
        reject(err);
      })
    }
  })
}

问:上边代码中能否直接将resolve传递给then方法,或者能否直接将reject传递给catch方法?

是可以的。当原来的promise变为成功状态时,会去调用then的函数参数,并将原来调用resolve时传递的参数作为then的回调函数的参数。所以可以写的更简洁:

function race(promiseArr) {
  return new Promise((resolve, reject) => {
    promiseArr.forEach(p => {
      Promise.resolve(p)
        .then(resolve)
        .catch(reject);
    });
  });
}

全局捕获promise异常

业务场景:团队开发中可能有同学忘记处理promise的异常,此时全局中应该对此异常进行捕获,做一个兜底。

new Promise((resolve, reject) => {
  throw new Error("出错了");
}).then((res) => {
  console.log(res);
});

window.addEventListener('unhandledrejection', event => {
  const {
    error, // 错误对象
    promise, // 出现异常的promise对象
  } = event
  console.log(error, promise)
})

对比Promise.race方法,实现一个last方法

描述:race方式是接收一个数组,只要其中有一个promise实例的状态率先改变,最终promise的实例就会改变。现在要对比着自定义实现一个last方法,不管前边的promise状态怎么变,我们只

据最后一个发生状态改变的promise来决定最终promise实例的状态。

new Promise((resolve, reject) => {
  throw new Error("出错了");
}).then((res) => {
  console.log(res);
});

window.addEventListener('unhandledrejection', event => {
  const {
    error, // 错误对象
    promise, // 出现异常的promise对象
  } = event
  console.log(error, promise)
})

实现并行发送请求的函数

要求实现一个send(list, n, callback)函数,该函数接收三个参数,第一个参数是存储url的数组,第一个参数是限制的并发请求数,第三个参数是回调函数。要求在发送请求的过程中收集数据

存入数组中,数据和请求发送的顺序要一一对应。最后所有请求完成时,调用callback([data1, data2, data3, ...])。本题假设所有请求都会被正常处理,有返回结果。

// 请求函数
function myFetch(url) {
  return new Promise(resolve => {
    const timeout = parseInt(Math.random() * 3 + 1);
    setTimeout(() => {
      resolve(url);
    }, timeout * 1000);
  });
}

// 实现的send函数
function send(list, n, callback) {
  let num = 0;
  let count = 0;
  const data = [];
  const fn = (url, index) => {
    num++;
    myFetch(url).then(res => {
      data[index] = res;
      if (num < list.length) {
        fn(list[num], num);
      }
      if (++count === list.length) {
        callback(data);
      }
    });
  };

  for (let i = 0; i < n && i < list.length; i++) {
    fn(list[i], i);
  }
}

// 测试
const list = [1, 2, 3, 4, 5, 6, 7, 8, 9];
function callback(data) {
  console.log(data);
}
send(list, 5, callback);
send(list, 15, callback);
posted @ 2022-09-19 22:04  凉宫春日大胜利  阅读(515)  评论(0)    收藏  举报