jannahuang / blog

MIT License
0 stars 0 forks source link

Promise 是什么 #16

Open jannahuang opened 2 years ago

jannahuang commented 2 years ago

Promise

以往的异步编程模式

假设有个异步操作:

let x = 3;
setTimeout(() => x = x + 4, 1000);

如果在后续代码需要使用x,异步执行的函数需要在更新 x 的值以后通知其他代码。 在早期的JavaScript 中,只支持定义回调函数来表明异步操作完成。

异步返回值

function double(value, callback) {
    setTimeout(() => callback(value * 2), 1000);
}
double(3, (x) => console.log(`I was given: ${x}`));

解决方法是给异步操作提供一个回调,异步返回值可作为参数传给回调函数。

失败处理 分别处理成功回调和失败回调:

function double(value, success, failure) {
    setTimeout(() => {
        try {
            if (typeof value !== 'number') {
                throw 'Must provide number as first argument';
            }
            success(2 * value);
        } catch (e) {
            failure(e);
        }
    }, 1000);
}
const successCallback = (x) => console.log(`Success: ${x}`);
const failureCallback = (e) => console.log(`Failure: ${e}`);

double(3, successCallback, failureCallback);
double('b', successCallback, failureCallback);

当有多个异步操作时,通常需要深度嵌套的回调函数(俗称“回调地狱”)来解决。随着代码越来越复杂,回调策略是不具有扩展性的。因此,为了解决异步问题,出现了 Promise。

Promise

Promise 对象的构造器(constructor)语法如下:

let promise = new Promise(function(resolve, reject) {
  // executor 要执行的代码
});

传递给 new Promise 的函数被称为 executor。它的参数 resolve 和 reject 是由 JavaScript 自身提供的回调。当 new Promise 被创建,executor 会自动运行。(事件循环考点)

当 executor 有结果时,它应该调用以下回调之一:

也就是 executor 会自动运行并尝试执行一项工作。尝试结束后,如果成功则调用 resolve,如果出现 error 则调用 reject。

Promise.resolve() promise 并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用 Promise.resolve() 静态方法,可以实例化一个解决的期约。下面两个期约实例实际上是一样的:

let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();

Promise.reject() 与 Promise.resolve() 类似,Promise.reject()会 实例化一个rejected promise 并抛出一个异步错误(这个错误不能通过try/catch 捕获,而只能通过拒绝处理程序 .then(null, f) 或 .catch(f) 捕获)。下面的两个期约实例实际上是 一样的:

let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();

由 new Promise 构造器返回的 promise 对象具有以下内部属性

executor 只能调用一个 resolve 或一个 reject,状态改变后不可再更改。 并且,resolve/reject 只需要一个参数(或不包含任何参数),并且将忽略额外的参数。也可以立即调用 resolve 或 reject。

then, catch

promise.then(
  function(result) { /* handle a successful result */ },
  function(error) { /* handle an error */ }
);

.then 的第一个参数是一个函数,该函数将在 promise resolved 且接收到结果后执行。 .then 的第二个参数也是一个函数,该函数将在 promise rejected 且接收到 error 信息后执行。 如果我们只关心 resolved 结果,可以只给 .then 提供一个参数。

promise.catch(
    function(error) { /* handle an error */ }
); 

如果我们只关心 rejected 结果,可以使用 .then(null, errorHandlingFunction),或者 .catch(errorHandlingFunction)。 .catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个简写形式,是一个语法糖。

finally

promise.finally(
    function() { /* handle finally */ }
); 

无论 promise 被 resolve 还是 reject,都会调用 .finally。.finally 可以避免 .then 和 .catch 处理程序中出 现冗余代码。但是 .finally 无法知道 promise 状态,主要用于添加清理代码。 finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。

Promise 链

因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回的是 promise,所以它们可以被链式调用。 链式调用

new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
  console.log(result); // 1
  return result * 2;
}).then(function(result) { // (***)
  console.log(result); // 2
  return result * 2;
})

但是单独多次添加到一个 promise 上,并不是 promise 链。它们不会相互传递 result,而是彼此独立运行处理任务。

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
  console.log(result); // 1
  return result * 2;
});
promise.then(function(result) {
  console.log(result); // 1
  return result * 2;
});

返回 promise

.then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise。其他的处理程序将等待它 settled 后再获得其结果。当它被 settled 后,其 result(或 error)将被进一步传递下去。

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000);

}).then(function(result) {
  console.log(result); // 1
  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  });
}).then(function(result) { // (**)
  console.log(result); // 2
})

使用 promise 进行错误处理

当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序。 .catch 不必是立即的。可以有许多个 .then 处理程序,然后在尾端使用一个 .catch 处理上面的所有 error。

隐式 try...catch

promise 的 executor 和处理程序周围有一个“隐式的 try..catch”。如果发生异常,它就会被捕获,并被视为 rejection 进行处理。

new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!

// 下述代码和上述代码效果完全相同
new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!

在处理程序中也一样。比如在 .then 处理程序中 throw,这意味着 promise rejected,因此控制权移交至最近的 error 处理程序。

new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  throw new Error("Whoops!"); // reject 这个 promise
}).catch(alert); // Error: Whoops!

再次抛出错误

如果在 .catch 中无法处理该 error,可以再次抛出错误 throw,那么控制权就会被移交到下一个最近的 error 处理程序。 如果处理该 error 并正常完成,那么它将继续到最近的成功的 .then 处理程序。

未处理的 rejection

如果出现 error,promise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序。 但如果没有相应的处理程序,JavaScript 引擎会跟踪此类 rejection,会生成一个全局的 error,脚本会报错,并在控制台留下信息。 在浏览器中,我们可以使用 unhandledrejection 事件来捕获这类 error:

window.addEventListener('unhandledrejection', function(event) {
  // 这个事件对象有两个特殊的属性:
  alert(event.promise); // 生成该全局 error 的 promise
  alert(event.reason); // 未处理的 error 对象
});

练习

  1. 以下 .catch 会被触发吗?
    new Promise(function(resolve, reject) {
    setTimeout(() => {
    throw new Error("Whoops!");
    }, 1000);
    }).catch(alert);

    不会触发。 函数代码周围有个“隐式的 try..catch”。所以,所有同步错误都会得到处理。 但是这里的错误并不是在 executor 运行时生成的,而是在 setTimout 稍后生成的。因此,promise 无法处理它。

Promise API

在 Promise 类中,有 6 种静态方法。其中最常用的是 Promise.all。

Promise.all

Promise.all 可以并行执行多个 promise,并等待所有 promise 都准备就绪,之后再进行处理。 Promise.all 接受一个可迭代对象(通常是一个数组项为 promise 的数组),并返回一个新的 promise。 当所有给定的 promise 都 resolve 时,新的 promise 才会 resolve,并且其结果数组将成为新 promise 的结果。结果数组中元素的顺序与其在源 promise 中的顺序相同。 注意:如果源 promise 数组中的任何一个不是 promise,那么它将被“按原样”传递给结果数组。

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(console.log); // 返回结果数组 [1,2,3],当上面这些 promise 准备好时:每个 promise 都贡献了数组中的一个元素

如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error。

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(console.log); // Error: Whoops!

如果其中一个 promise 被 reject,Promise.all 就会立即被 reject,完全忽略列表中其他的 promise。它们的结果也被忽略。

其他情况:

let p1 = Promise.all([]); // 空的可迭代对象等价于Promise.resolve(),在结果数组中返回 undefined

let p2 = Promise.all(); // 无效的语法
// TypeError: cannot read Symbol.iterator of undefined 

Promise.allSettled(新增的特性)

Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何都返回结果数组。 结果数组包括:

Promise.race

Promise.race 只等待第一个 settled 的 promise 并获取其结果(或 error),其他的 settled 将被忽略。

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(console.log); // 1

Promise.any

Promise.any 只等待第一个 fulfilled 的 promise,并将这个 fulfilled 的 promise 返回。 如果给出的 promise 都 rejected,那么返回的 promise 会带有 AggregateError —— 一个特殊的 error 对象,在其 errors 属性中存储着所有 promise error。

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(console.log); // 1

Promise.resolve

Promise.resolve(value) 用结果 value 创建一个 resolved 的 promise。 效果等同于:

let promise = new Promise(resolve => resolve(value));

Promise.reject

Promise.reject(error) 用 error 创建一个 rejected 的 promise。 效果等同于:

let promise = new Promise((resolve, reject) => reject(error));

实际上,这个方法几乎从未被使用过。(谁没事会创建被 reject 的 promise...)

async/await

async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。

async function

在函数前面的 “async” 表示:这个函数总是返回一个 promise。其他内容将自动被包装在一个 resolved 的 promise 中。

async function f() {
  return 1;
}

// 上述代码与下述代码效果相同
async function f() {
  return Promise.resolve(1);
}

await

await 只在 async 函数内工作。关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。

async function f() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000)
  });
  let result = await promise; // 等待,直到 promise resolve (*)
  console.log(result); // "done!"
}

f()

await 暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。

async/await 和 promise.then/catch 当使用 async/await 时,几乎就不会用到 .then 了,因为 await 处理了等待。并处理 error 会使用常规的 try..catch 而不是 .catch。这通常(但不总是)更加方便。

async function f() {
  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
  } catch(err) {
    // 捕获到 fetch 和 response.json 中的错误
    alert(err);
  }
}
f();

async/await 可以和 Promise.all 一起使用

// 等待结果数组
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
]);

练习

  1. 使用 async/await 改写下述代码
    
    function loadJson(url) {
    return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new Error(response.status);
      }
    });
    }

loadJson('https://javascript.info/no-such-user.json') .catch(alert); // Error: 404

答案:
```javascript
// 1. 函数前加 async
async function loadJson(url) {
  //2. 将 .then 替换成 await
  let response = await fetch(url)
  if (response.status == 200) {
    // 3. 可以直接 return 或者 await 后 return
    let json = await response.json()
    return json
  }
  // 4. 处理错误
  throw new Error(response.status)
}

loadJson('https://javascript.info/no-such-user.json')
  .catch(alert); // Error: 404
  1. 在普通函数中调用 async 函数
    
    async function wait() {
    await new Promise(resolve => setTimeout(resolve, 1000));
    return 10;
    }

function f() { // ……这里你应该怎么写? // 我们需要调用 async wait() 并等待以拿到结果 10 // 记住,我们不能使用 "await" }

答案:
```javascript
function f() {
  // 1 秒后显示 10
  wait().then(result => alert(result));
}

只需要把 async 调用当作 promise 对待,并在它的后面加上 .then 即。

以上笔记参考《现代 JavaScript 教程》,《JavaScript高级程序设计(第4版)》及 MDN 文档