深入理解 Promise:手把手带你掌握异步编程利器

283 阅读8分钟

前言

在前后端交互过程中,异步请求是必不可少的。在没有 Promise 之前,前端处理异步请求的解决方案大多是通过传入回调函数,就比如下面这样的示例代码。

function requestFn(name, successCallback, failureCallback) {
  // 用 setTimeout 模拟异步请求
  setTimeout(() => {
    // 此处获取到结果后,外部需要知道
    if (name === 'a') {
      successCallback('调用成功');
    } else {
      failureCallback('调用失败');
    }
  }, 1000);
}

requestFn(
  'a',
  (res) => {
    // 当定时器时间到了后,回调这个函数,打印如下数据

    // data: 调用成功
    console.log('data:', res);
  },
  (err) => {
    console.log('err', err);
  },
);

这种方式也能处理异步请求,但是存在如下缺点:

  1. 代码编写麻烦:自己编写这样一个 requestFn 时,必须要命名好相关的参数名字,以及在合适位置进行调用回调函数
  2. 代码不规范:使用别人开发的 requestFn 时,就像一个黑盒子一样,自己在使用时就必须去查看相关源码或文档,才能知道对应回调函数的参数怎么传,以及如何才能拿到对应的返回结果
  3. 回调地狱:在开发中,经常会遇到连续执行多个异步请求。当上一个请求执行成功后,才执行下一个请求,这样就会存在函数的回调参数中再嵌套一层请求的函数,非常麻烦。

于是,为了解决回调函数的这些问题,就有了新的异步处理方式:Promise

Promise 基本概念

  • Promise 是一个类,需要 new Promise() 调用
  • Promise 是 ES6 发布的
  • Promise 用于解决异步编程问题
  • Promise 可以统一规范,减少沟通成本

Promise 基本用法

使用方式:new Promise(executor)
executor 是一个函数,接收两个函数作为参数:resolvereject。在请求成功时,调用 resolve。在请求失败时,调用 reject

示例代码如下:

function fn(name) {
  // 调用此函数直接返回一个 promise 对象
  return new Promise((resolve, reject) => {
    // 模拟异步请求
    setTimeout(() => {
      if (name === 'a') {
        resolve('调用成功');
      } else {
        reject('调用失败');
      }
    }, 1000);
  });
}

// 在这里调用后,会拿到一个 promise 对象
// 调用 promise.then 获取调用成功的结果
// 调用 promise.catch 获取调用失败的结果
const newPromise = fn('a');

newPromise
  .then((res) => {
    // res 调用成功
    console.log('res', res);
  })
  .catch((err) => {
    console.log('err', err);
  });

Promise 三种状态

MDN Promise 三种状态

在 Promise 的不同阶段,会有如下三种状态:

  1. 待定 (pending)
  2. 已完成(fulfilled)
  3. 已拒绝(rejected)
  • 注意:Promise 的状态一旦确定就不会再次改变,只有如下改变方式
    • pending -> fulfilled
    • pending -> rejected

示例代码如下:

new Promise((resolve, reject) => {
  // FIXME:在执行 resolve 和 reject 回调之前,Promise 状态都一直是 pending
  let a = 1;
  console.log('a', a);

  // 调用 resolve 后,Promise 状态就会从 pending 变为 fulfilled
  resolve();

  // 因为 Promise 的状态一旦改变,就已经确定了
  // 这后续执行的代码,Promise 状态也会是 fulfilled
  let b = 2;
  console.log('b', b);

  // 即使这里调用 reject 也不会修改 Promise 状态
  // reject()
}).then(
  (res) => {
    // FIXME: 到了这个地方之后,Promise 状态就变成 fulfilled
    console.log(res);
  },
  (err) => {
    // FIXME: 到了这个地方之后,Promise 状态就变成 rejected
    console.log(err);
  },
);

resolve 参数详解

resolve 参数有三种类型:

  1. 普通数据类型:字符串、普通对象 等
const promise1 = new Promise((resolve, reject) => {
  resolve(111);
});

promise1.then((res) => {
  // res 111
  console.log('res', res);
});
  1. promise 对象
const promise2 = new Promise((resolve, reject) => {
  // 当 resolve 中的参数为 promise 对象时,则当前这个 new Promise 的状态将会由此传入的这个 promise 对象的状态决定
  // 我觉得就相等于是一个拦截操作,权限移交的操作

  resolve(
    new Promise((resolve, reject) => {
      // resolve('promise data');

	 // 这里调用就 reject 则,后续就需要捕获错误
      reject('promise err');
    }),
  );
});

promise2.then(
  (res) => {
    // res promise data
    console.log('res', res);
  },
  (err) => {
    // err promise err
    console.log('err', err);
  },
);
  1. Thenable 对象 MDN 解析 Thenable 对象

    什么是 Thenable 对象?

    • 一个对象中有 then 方法,且该方法有两个回调函数作为参数。第一个参数用于在已完成时调用,第二个参数用于在已结束时调用。
    • 其实这个地方和 new Promise((resolve, reject) => {}) 是一致的。
const thenableObj = {
  then(resolve, reject) {
    resolve('thenable resolve');

    // reject('thenable reject');
  },
};

const promise3 = new Promise((resolve, reject) => {
  // 传入 thenable 的情况和 promise 情况一致
  // 当前这个 new Promise 的状态,由 resolve 中传入的 thenable 的状态决定
  // 相当于就是拦截操作,权限移交的操作
  resolve(thenableObj);
});

promise3.then(
  (res) => {
    // res thenable resolve
    console.log('res', res);
  },
  (err) => {
    // err thenable reject
    console.log('err', err);
  },
);

Promise 对象方法

  • Promise 对象方法是需要先 new Promise 之后,调用对象的方法

then

(1) 可链式调用

const promise = new Promise((resolve, reject) => {
  resolve('promise');
});

promise.then(res1 => console.log(res1)).then(res2 => console.log(res2))

(2) 可直接在 then(resCallback, errCallback),可直接在 errCallback 中捕获异常错误

  • 缺点:代码不够清晰
const promise = new Promise((resolve, reject) => {
  resolve('promise');
});

promise.then(res => {
	console.log(res)
},
 err => {
	console.log(err)
})

(3) promise.then 的返回值

  • promise.then 会返回一个 Promise
  • promise.then 返回值,一共有如下三种情况

(3.1) 返回普通数据

  • 内部为 Promise 包裹了一下,然后走的 resolve 流程
const promise = new Promise((resolve, reject) => {
  resolve('promise');
});

promise
  .then((res) => {
    return 'aaa';
  })
  .then((res) => {
    // then 普通返回值 aaa
    console.log('then 普通返回值', res);
  });

(3.2) 返回值为 Promise

  • 后续的调用的状态由这个返回的 Promise 内部的状态决定
const promise = new Promise((resolve, reject) => {
  resolve('promise');
});

promise
  .then((res) => {
    return new Promise((resolve, reject) => {
      reject('错误');
    });
  })
  .then(
    (res) => {
      console.log('res', res);
    },
    (err) => {
      // err 错误
      console.log('err', err);
    },
  );

(3.3) 返回值为 thenable

  • 后续的调用的状态由这个返回的 thenable 内部的状态决定
promise
  .then((res) => {
    const obj = {
      then(resolve, reject) {
        reject('thenable 错误');
      },
    };

    return obj;
  })
  .then(
    (res) => {
      console.log('res', res);
    },
    (err) => {
      // err thenable 错误
      console.log('err', err);
    },
  );

catch

(1) 直接捕获 promise 异常

const promise = new Promise((resolve, reject) => {
  reject('promise 错误');
});

promise.catch((err) => {
  // err promise 错误
  console.log('err', err);
});

(2) 链式调用捕获 promise 异常

  • 此处 .catch,优先捕获 promise 的错误
  • 如果 promise 是正常的,才开始捕获 promise.then 中的错误
  • 也就是先来后到的原则
  • 个人感悟:我这几天写这个代码的时候,分析的时候发现,我现在只要理清楚代码的逻辑就可以了,我不管用什么比喻的方式来阐述这个道理,只要能讲清楚就可以了
const promise = new Promise((resolve, reject) => {
  reject('promise 错误');
});

promise
  .then((res) => {
    return new Promise((resolve, reject) => {
      reject('promise.then 错误');
    });
  })
  .catch((err) => {
    // err promise 错误
    console.log('err', err);
  });

finally

  • 不管是 fulfilled 和 rejected 最后都会调用 finally 方法
const promise = new Promise((resolve, reject) => {
  reject('promise 错误');
});

promise
  .then((res) => {
    console.log('res', res);
  })
  .catch((err) => {
    // err promise 错误
    console.log('err', err);
  })
  .finally(() => {
    // finally 调用
    console.log('finally 调用');
  });

Promise 类方法

  • Promise 类方法是直接调用 Promise 类上面的方法即可

resolve

用法:Promise.resolve()

const obj = {
  name: 'aaa',
};

// 1. 入参为普通类型
const p1 = Promise.resolve(obj);

p1.then((res) => {
  // res1 { name: 'aaa' }
  console.log('res1', res);
});

// 等价于
// 说白了就是语法糖,然后 Promise 内部处理过,提供了一个简便的方式

const p2 = new Promise((resolve, reject) => {
  resolve(obj);
});

p2.then((res) => {
  // res2 { name: 'aaa' }
  console.log('res2', res);
});

// 2. 入参为 promise
const p3 = new Promise((resolve, reject) => {
  reject('promise');
});

const p4 = Promise.resolve(p3);

p4.then(
  (res) => {
    console.log('res', res);
  },
  (err) => {
    // err promise
    console.log('err', err);
  },
);

// 3. 入参为 thenable
const thenObj = {
  then(resolve, reject) {
    reject('thenable');
  },
};

const p5 = Promise.resolve(thenObj);

p5.then(
  (res) => {
    console.log('res', res);
  },
  (err) => {
    // err thenable
    console.log('err', err);
  },
);

reject

用法:Promise.reject()

  • Promise.reject 中的状态,始终会是 rejected 状态,和传入的类型无关
const p1 = new Promise((resolve, reject) => {
  resolve('测试');
});

const p2 = Promise.reject(p1);

p2.then(
  (res) => {
    console.log('res', res);
  },
  (err) => {
    // err Promise { '测试' }
    console.log('err', err);
  },
);

all

用法:Promise.all(promiseList)

  • 要么 promiseList 所有状态都是 fulfilled
  • 要么 promiseList 中一旦有一个 reject 中断后,则走 catch 方法
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p1 成功');
  }, 1000);
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    // resolve('p2 成功');

    // FIXME: 一旦其中有一个 reject 中断之后,则直接将此 reject 的数据返回
    reject('p2 错误');
  }, 2000);
});

const pList = Promise.all([p1, p2]);

pList.then(
  (res) => {
    // pList 中所有 promise 都为 fulfilled 则走此处

    // 打印:res [('p1 成功', 'p2 成功')];
    console.log('res', res);
  },
  (err) => {
    // pList 中一旦其中有一个 reject 中断之后,则走此处

    // 打印:err p2 错误
    console.log('err', err);
  },
);

allSettled

用法:Promise.allSettled(promiseList)

  • 等待所有状态都敲定后,再走 then 方法
  • fulfilled 状态:{ status: 'fulfilled', value: 111 }
  • rejected 状态:{ status: 'rejected', reason: 222 }
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(111);
  }, 100);
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(222);
  }, 200);
});

const pList = Promise.allSettled([p1, p2]);

pList.then((res) => {
  // res [({ status: 'fulfilled', value: 111 }, { status: 'rejected', reason: 222 })];
  console.log('res', res);
});

race

用法:Promise.race(pList)

  • 获取第一个 resolve 的数据
  • 如果 reject 比 resolve 先获取,则走 catch 就结束了
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(111);
  }, 100);
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(222);
  }, 200);
});

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(333);
  }, 30);
});

const pList = Promise.race([p1, p2, p3]);

pList.then(
  (res) => {
    console.log('res', res);
  },
  (err) => {
    // err 333
    console.log('err', err);
  },
);

any

用法:Promise.any(promiseList)

  • 以获取到第一个 resolve 为结束的标志,reject 不会终止执行效果
  • 如果全都是 rejected 则打印如下:err [AggregateError: All promises were rejected] { [errors]: [ 333 ] }
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(111);
  }, 100);
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(222);
  }, 200);
});

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(333);
  }, 30);
});

Promise.any([p1, p2, p3]).then(
  (res) => {
    // res 222
    console.log('res', res);
  },
  (err) => {
    console.log('err', err);
  },
);

参考资料