JavaScript Promise

JavaScript Promise

简单介绍一下 Promise 以及他的使用、异常处理、同步处理等等…

介绍

  我们都知道 JavaScript 是一种同步编程语言,上一行出错就会影响下一行的执行,但是我们需要数据的时候总不能每次都等上一行执行完成,这时就可以使用回调函数让它像异步编程语言一样工作。
像 NodeJS 就是采用异步回调的方式来处理需要等待的事件,使得代码会继续往下执行不用在某个地方等待着。但是也有一个不好的地方,当我们有很多回调的时候,比如这个回调执行完需要去执行下个回调,然后接着再执行下个回调,这样就会造成层层嵌套,代码不清晰,很容易进入“回调监狱”。。。
所以 ES6 新出的 Promise 对象以及 ES7 的 async、await 都可以解决这个问题。
Promise 是用来处理异步操作的,可以让我们写异步调用的时候写起来更加优雅,更加美观便于阅读。Promise 为承诺的意思,意思是使用 Promise 之后他肯定会给我们答复,无论成功或者失败都会给我们一个答复,所以我们就不用担心他跑了哈哈。
Promise 有三种状态:pending(未决定),resolved(完成fulfilled),rejected(失败)。只有异步返回时才可以改变其状态,因此我们收到的 Promise 过程状态一般只有两种:pending->fulfilled 或者 pending->rejected

使用

简单使用

直接上代码

function promiseTest(boolType = true) {
  return new Promise(function (resolve, reject) {
    // do something 然后返回一个 Promise 对象
    if (boolType) {
      resolve('成功');
    } else {
      reject('失败');
    }
  });
}
// Promise 的 then 接受两个参数
// 第一个是成功的 resolved 的成功回调
// 另一个是失败的 rejected 的失败回调【可选】。
// 并且 then 也可以返回 Promise 对象,这样就可以实现链式调用。
// 栗子如下
promiseTest(true).then((value) => console.log(`${value}后的处理A`));
promiseTest(false).then(
  (value) => console.log(`${value}后的处理B`),
  (value) => console.log(`${value}后的处理B`)
);
promiseTest(false).catch((value) => console.log(`${value}后的处理C`));

// 链式调用,这种写法是不是比我们嵌套回调地狱优美多啦~
promiseTest(false)
  .catch((value) => promiseTest(true))
  .then(() => console.log('第一次调用失败后尝试第二次成功了!'));
// catch 不仅可以捕获失败和 return Promise,也可以捕获异常。
promiseTest(true)
  .then((value) => value1)
  .catch((e) => console.log(e));

/* ---打印结果--- */
成功后的处理A
失败后的处理B
失败后的处理C
第一次调用失败后尝试第二次成功了!
ReferenceError: value1 is not defined at ...
/* ---打印结果--- */

另外当我们需要在方法中等待 Promise 返回时,需要给方法添加 async 修饰,并使用 await 等待。

async function asyncFunc() { // 只要添加了 async 关键字,该方法的返回值就是一个 Promise。
  let result = await new Promise((resolve, reject) => {
    setTimeout(() => resolve(123), 2000);
  });
  return result;
}
asyncFunc(); // Promise {<pending>}
asyncFunc().then((value) => console.log(value)); // 123
await asyncFunc(); // 123

Api 方法

Promise.resolve

将现有对象转为 Promise 对象 resolved,Promise.resolve(‘test’) 相当于 new Promise((resolve) => resolve(‘test’));

Promise.reject

将现有对象转为 Promise 对象 rejected,Promise.rejected(‘test’) 相当于 new Promise((rejected) => rejected(‘test’));

Promise.prototype.then

then() 方法返回一个 Promise,它最多需要有两个参数:Promise 的成功和失败情况的回调函数。

// promiseTest.then(onFulfilled[, onRejected]);
promiseTest.then(value => {
  // fulfillment
}, reason => {
  // rejection
});

  • onFulfilled 可选
    • 当 Promise 变成接受状态(fulfilled)时调用的函数。该函数有一个参数,即接受的最终结果(the fulfillment value)。
    • 如果该参数不是函数,则会在内部被替换为 (x) => x,即原样返回 promise 最终结果的函数。
  • onRejected 可选
    • 当 Promise 变成拒绝状态(rejected)时调用的函数。该函数有一个参数,即拒绝的原因(rejection reason)。
    • 如果该参数不是函数,则会在内部被替换为一个 “Thrower” 函数 (it throws an error it received as argument)。

Promise.prototype.catch

catch() 方法返回一个 Promise,并且处理拒绝的情况。它的行为与调用 Promise.prototype.then(undefined,onRejected) 相同。事实上调用 obj.catch(onRejected) 其实就是 obj.then(undefined, onRejected)

// promiseTest.catch(onRejected);
promiseTest.catch(function(reason) {
   // 拒绝/异常处理
});

  • onRejected
    • 当 Promise 被 rejected 时,被调用的一个Function。该函数拥有一个参数:reason/rejection 的原因。
    • 如果 onRejected 抛出一个错误或返回一个本身失败的 Promise,通过 catch() 返回的 Promise 被 rejected。否则,它将显示为成功(resolved)。

Promise.prototype.finally

finally() 方法返回一个 Promise。在 Promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在 then()catch()各写一次 的情况。

promiseTest.finally(() => {
  // do my things
});

promiseTest.then(
  (result) => {
    // do my things
    return result;
  },
  (error) => {
    // do my things
    throw error;
  }
);

Promise.allSettled

该 Promise.allSettled() 方法返回一个在所有给定的 Promise 都已经 fulfilled 或 rejected 后的 Promise,并带有一个对象数组,每个对象表示对应的 Promise 结果。

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(() => reject('test'), 1000));
const promises = [promise1, promise2];
Promise.allSettled(promises).then((results) => results.forEach((result) => console.log(result.status)));

/* ---打印结果--- */
fulfilled
rejected
/* ---打印结果--- */

Promise.all

Promise.all() 方法接收一个 Promise 的 iterable 类型(Array,Map,Set都属于 ES6 的 iterable 类型)的输入,并且只返回一个 Promise 实例,那个输入的所有 Promise 的 resolve 回调的结果是一个数组。
它的 resolve 回调执行是在所有输入的 Promise 的 resolve 回调都结束,或者输入的 iterable 里没有 Promise 了的时候。
它的 reject 回调执行是只要任何一个输入的 Promise 的 reject 回调执行或者输入不合法的 Promise 就会立即抛出错误,并且 reject 的是第一个抛出的错误信息。

/// 当我们需要同步执行多个 Promise 的时候,可以使用 Promise.all() 来"并发请求",减少等待时间。
/// 举个简单的栗子:
/// 假设我需要三次请求获取数据,然后渲染页面。那么我们看一下使用 Promise.all 和不使用的区别。
console.time('不使用Promise.all');
let a = await new Promise((resolve, reject) => {
  setTimeout(function () {
    // 模拟请求第一笔数据
    resolve('123');
  }, 1000);
});
let b = await new Promise((resolve, reject) => {
  setTimeout(function () {
    // 模拟请求第一笔数据
    resolve('456');
  }, 2000);
});
let c = await new Promise((resolve, reject) => {
  setTimeout(function () {
    // 模拟请求第一笔数据
    resolve('789');
  }, 3000);
});
console.log(a, b, c);
console.timeEnd('不使用Promise.all');

console.time('使用Promise.all');
function all() {
  return Promise.all([
    new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve('123');
      }, 1000);
    }),
    new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve('456');
      }, 2000);
    }),
    new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve('789');
      }, 3000);
    })
  ]);
}
console.log(...(await all()));
console.timeEnd('使用Promise.all');

/* ---打印结果--- */
123 456 789
不使用Promise.all: 8569.14794921875 ms
123 456 789
使用Promise.all: 3006.345947265625 ms
/* ---打印结果--- */

  • 我们可以看到,不使用 all 的情况下我们需要等待的时间会长很多,而使用 all 之后,我们的请求相当于并发,大大节约了时间。

Promise.race

Promise.race(iterable) 方法返回一个 Promise,一旦迭代器中的某个 Promise 解决或拒绝,返回的 Promise 就会解决或拒绝。

/// 这个其实就是赛道的意思,哪个 Promise 先完成,就返回哪个。
/// 举个简单的栗子:
/// 假设我们需要从三台服务器上拿取数据,那么那台先返回我们就用哪台的数据。
function race() {
  return Promise.race([
    new Promise((resolve, reject) => {
      // 第一台服务器 1s
      setTimeout(function () {
        resolve('123');
      }, 1000);
    }),
    new Promise((resolve, reject) => {
      // 第一台服务器 2s
      setTimeout(function () {
        resolve('456');
      }, 2000);
    }),
    new Promise((resolve, reject) => {
      // 第一台服务器 3s
      setTimeout(function () {
        resolve('789');
      }, 3000);
    })
  ]);
}
console.time('raceTime');
console.log(await race());
console.timeEnd('raceTime');


/* ---打印结果--- */
123
raceTime: 1056.11083984375 ms
/* ---打印结果--- */

Promise.any

Promise.any() 接收一个 Promise 可迭代对象,只要其中的一个 Promise 成功,就返回那个已经成功的 Promise。如果可迭代对象中没有一个 Promise 成功 (即所有的 Promise 都失败/拒绝),就返回一个失败的 Promise 和 AggregateError 类型的实例它是 Error 的一个子类,用于把单一的错误集合在一起。本质上,这个方法和 Promise.all() 是相反的。

{% note warning %}
注意:Promise.any() 方法依然是实验性的,尚未被所有的浏览器完全支持。它当前处于 TC39 第四阶段草案。
{% endnote %}

  Promise.any() 与 Promise.race() 方法不同,Promise.race() 方法主要关注 Promise 是否已解决,而不管其被解决(成功)还是被拒绝(失败)。所以使用 Promise.any 来获取多台服务器数据时会更合理。

优雅的进行异常处理

详解

  • 之前刷视频有看到一些小问题:
    • 使用多个 await 时,前一个出现异常,如何不影响后续执行?
    • 我们每次使用 Promise 都需要处理异常吗?
    • 如何统一处理异常和捕获异步异常呢?
/// 我们先定义几个函数来测试
function test1() {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      console.log('test1');
      resolve('test1');
    }, 1000); // 正常 1s 执行完毕并成功
  });
}

function test2() {
  return new Promise((resolve, reject) => {
    var x = abc + 1;  // 出现异常的情况
    console.log('test2');
    resolve('test2');
  });
}

function test3() {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      try {
        var y = abcabc + 1;
        resolve(y);
      } catch (e) {
        console.log('不属于 Promise 内部错误,请自己包裹。');
        console.log('不包裹则会冒泡到 window.onerror,若再未处理则报错到控制台。示例:test4!');
        reject('test3 error');
      }
    }, 1000);
  });
}

function test4() {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      var z = abcabcabc + 1;
      console.log(z);
    }, 1000);
    reject('test4 error');
  });
}

  • 首先我们看第一个问题,如果我们直接这样执行,那么由于 test2() 出现错误,test1() 肯定是无法执行的。
await test2();
await test1();

  • 这时候我们需要这样写,但是这样虽然可以解决这个问题,但是如果前面的 Promise 数量一多,那么可读性就大大降低了!
await test2().catch((e) => console.log(e));
await test1();
或
try {
  await test2();
} catch (e) {
  console.log(e);
}
await test1();

  • 再结合后面两个问题,我查看了一些资料,包括 Dima Grossman 的 to.js,所以我们可以采用终极方案,话不多说直接上代码。
/**
 * 首先我参考了 to.js,扩展 Promise 原型方法,用来直接帮助执行且处理异常。
 * @param {Function} res
 * @param {Function} rej
 * @returns
 */
Promise.prototype.to = function (res, rej) {
  return this.then((data) => {
    res && res(data);
    // console.log(data);
    return data;
  }).catch((err) => {
    rej && rej(err); //可去除此行,全局定义处理错误函数,用以解决第三个问题。
    console.log(err); // 如果没定义前面的 rej 回调处理函数,我们可以帮助处理,例如此处可以帮我们处理 test2 的异常。
  });
};
/**
 * 全局捕获异常
 * @param {object} message
 * @param {object} source
 * @param {object} lineno
 * @param {object} colno
 * @param {object} error
 * @returns
 */
window.onerror = function (message, source, lineno, colno, error) {
  console.log('捕获到异常:', { message, source, lineno, colno, error });
  //do something 全局处理
  return true; // return true 不在控制台报错
};
/// 这个可以帮助我们捕获 test4 setTimeout 中的异步异常。

此时我们再如此执行,均不会报错。

await test1();
await test2();
await test3();
await test4();
console.log('前面报错不会执行');

test1();
test2();
test3();
test4();
console.log('前面报错不会执行');

await test1().to();
await test2().to();
await test3().to();
await test4().to();
console.log('前面报错依然会执行');

test1().to((x) => console.log(`自定义处理的${x}`)); // 如果需要自定义处理也可以传入回调函数,我们的扩展 to 原型方法跟 then 一样是支持两个参数的。
test2().to();
test3().to();
test4().to(); 
console.log('前面报错依然会执行');

多说几句

另外补充一下,说到 Promise 的优雅处理,我们平时写的时候前往不要像下面一样嵌套使用。

function request1() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve('result1');
    }, 1000);
  });
}

function request2(need1) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(need1 + 'result2');
    }, 1000);
  });
}

function request3(need2) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(need2 + 'result3');
    }, 1000);
  });
}

request1().then((res1) => {
  request2(res1).then((res2) => {
    request3(res2).then((res3) => {
      console.log(res3);
    });
  });
});
// 这种写法可读性太差且不好维护

  而应该是每次调用 then 方法后,在 then 方法中 return 下一次需要用到的数据。然后 then 方法会返回一个 Promise 实例,再继续使用 then 通过 res 参数可以获取上一次 return 的数据,并在该 then 方法中发送后续的异步请求,这样就达到了我们之前说过的链式调用传递效果,而且 reject 抛出错误的时候,只需在最后 catch 一层就可以了,这样无论是哪个 then reject 了,都会在最后的 catch 这里捕获到错误

request1()
  .then((res1) => request2(res1))
  .then((res2) => request3(res2))
  .then((res3) => console.log(res3))
  .catch((e) => console.log('异常处理', e));
// 没错就是这样,作为强迫症程序员,就是要优雅(*v*)!

实现 Promise Retry

Promise.prototype.retry = function (count = 0, delay = 0) {
  return new Promise((resolve, reject) => {
    this.then((res) => {
      resolve(res);
    }).catch(async (e) => {
      if (count > 0) {
        // 此处也可使用 setTimeout 实现
        await Promise.prototype.sleep(delay);
        --count;
        console.log('重试', count);
        resolve(this.retry(count, delay));
      } else {
        reject('重试结束');
      }
    });
  });
};

Promise.prototype.sleep = function (milliseconds) {
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
};

new Promise((resolve, reject) => reject('test')).retry(3, 1000);

提一下 yield*

参考文章,虽然与本文无关,但是记录一下。

  yield * 表达式用于委托给另一个 generator 或可迭代对象。表达式迭代操作数,并产生它返回的每个值。我们可以看成使用此关键字让方法一步步执行,他会返回一个对象包含 value(返回值) 和 done(是否完成)。

  • 栗子
function* yieldFunc(a, b, c) {
  yield* [4, 5, 6];
  yield* arguments;
  console.log('打印参数后的第一步');
  yield 'hello world';
  console.log('即将结束');
  yield '下一步结束';
  console.log('结束');
}

let runFuncs = yieldFunc(1, 2, 3);

runFuncs.next(); // {value: 4, done: false}
runFuncs.next(); // {value: 5, done: false}
runFuncs.next(); // {value: 6, done: false}
runFuncs.next(); // {value: 1, done: false}
runFuncs.next(); // {value: 2, done: false}
runFuncs.next(); // {value: 3, done: false}
runFuncs.next(); // 打印参数后的第一步,{value: "hello world", done: false}
runFuncs.next(); // 即将结束,{value: "下一步结束", done: false}
runFuncs.next(); // 结束,{value: undefined, done: true}

// 假如我们一个验证需要多步,我们可以给 next() 传参,传递的值在原函数体中会变成上步得到的结果。
function* test(a, b) {
  const x = yield (a + b);
  // x 的值是我们根据第一步的结果判断后,通过 next 传递给他的。
  const y = yield x == 2; // 例如此处:xxx.next(6) 则 x = 6; xxx.next(7) 则 x = 7; 而不管我们传递的 a b 是什么。
  let z = 'hello world';
  if (y) {
    console.log('认证成功!');
    z = '已登录';
  } else {
    console.log('认证失败!');
    z = '未登录';
  }
  return z;
}


let authTest = test(1, 1);
let hasNext = authTest.next();
console.log(hasNext);
while (!hasNext.done) {
  hasNext = authTest.next(hasNext.value)
  console.log(hasNext);
}
// {value: 2, done: false}
// {value: true, done: false}
// 认证成功!
// {value: '已登录', done: true}

let authTestTrue = test(1, 1);
let next = authTestTrue.next();
console.log(next); // {value: 2, done: false}
next = authTestTrue.next(100);
console.log(next); // {value: false, done: false}
next = authTestTrue.next(true);
// 认证成功!
console.log(next); // {value: '已登录', done: true}

经验法则

  • 使用异步或阻塞代码时,请使用 Promise。
  • 为了代码的可读性,resolve 方法对应 then, reject 对应 catch。
  • 确保同时写入 .catch.then 方法来实现所有的 Promise。
  • 如果在 resolve/reject 两种情况下都需要做一些事情,请使用 .finally
  • 我们每次改变单个 Promise (单一原则)。
  • 我们可以在一个 Promise 中添加多个处理程序。
  • Promise 对象中所有方法的返回类型,无论是静态方法还是原型方法,都是 Promise。
  • Promise.all 中,无论哪个 Promise 首先未完成,Promise 的顺序都保持在值变量中。

基础部分参考公众号:前端小智

本文使用 文章同步助手 同步

posted @ 2023-09-28 11:11  pandaoh  阅读(2)  评论(0编辑  收藏  举报  来源