JavaScript Promise

2023-12-11 20:47:52 浏览数 (2)

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

介绍

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

使用

简单使用

直接上代码

代码语言:javascript复制
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 等待。

代码语言:javascript复制
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 的成功和失败情况的回调函数。

代码语言:javascript复制
// 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)

代码语言:javascript复制
// 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()各写一次 的情况。

代码语言:javascript复制
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 结果。

代码语言:javascript复制
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 的是第一个抛出的错误信息。

代码语言:javascript复制
/// 当我们需要同步执行多个 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 就会解决或拒绝。

代码语言:javascript复制
/// 这个其实就是赛道的意思,哪个 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() 是相反的。

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

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

优雅的进行异常处理

详解

  • 之前刷视频有看到一些小问题:
    • 使用多个 await 时,前一个出现异常,如何不影响后续执行?
    • 我们每次使用 Promise 都需要处理异常吗?
    • 如何统一处理异常和捕获异步异常呢?
代码语言:javascript复制
/// 我们先定义几个函数来测试
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() 肯定是无法执行的。
代码语言:javascript复制
await test2();
await test1();
  • 这时候我们需要这样写,但是这样虽然可以解决这个问题,但是如果前面的 Promise 数量一多,那么可读性就大大降低了!
代码语言:javascript复制
await test2().catch((e) => console.log(e));
await test1();
或
try {
  await test2();
} catch (e) {
  console.log(e);
}
await test1();
  • 再结合后面两个问题,我查看了一些资料,包括 Dima Grossman 的 to.js,所以我们可以采用终极方案,话不多说直接上代码。
代码语言:javascript复制
/**
 * 首先我参考了 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 中的异步异常。

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

代码语言:javascript复制
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 的优雅处理,我们平时写的时候前往不要像下面一样嵌套使用。

代码语言:javascript复制
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 这里捕获到错误

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

实现 Promise Retry

代码语言:javascript复制
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(是否完成)。

  • 栗子
代码语言:javascript复制
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 的顺序都保持在值变量中。

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

0 人点赞