nitroge / memories

日常学习累积的点点滴滴滴滴点点
7 stars 0 forks source link

Promise,async/await #18

Open nitroge opened 2 years ago

nitroge commented 2 years ago

Promise,async/await

简介:回调

function loadScript(src) {
  // 创建一个 <script> 标签,并将其附加到页面
  // 这将使得具有给定 src 的脚本开始加载,并在加载完成后运行
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}

脚本加载是异步的,但我们希望了解脚本何时加载完成,以使用其中的新函数和变量。
让我们添加一个 callback 函数作为 loadScript 的第二个参数,该函数应在脚本加载完成时执行:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}

这被称为“基于回调”的异步编程风格。异步执行某项功能的函数应该提供一个 callback 参数用于在相应事件完成时调用。

我们如何依次加载两个脚本:第一个,然后是第二个?
自然的解决方案是将第二个 loadScript 调用放入回调中,如下所示:

loadScript('/my/script.js', function(script) {
  alert(`Cool, the ${script.src} is loaded, let's load one more`);

  loadScript('/my/script2.js', function(script) {
    alert(`Cool, the second script is loaded`);
  });
});

乍一看,这是一种可行的异步编程方式。的确如此,对于一个或两个嵌套的调用看起来还不错。但对于一个接一个的多个异步行为,代码将会进入‘回调地狱’。
幸运的是,有其他方法可以避免。最好的方法之一就是 “promise”

Promise

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

let promise = new Promise(function(resolve, reject) {
  // executor()
});

传递给 new Promise 的函数被称为 executor。当 new Promise 被创建,executor 会自动运行。它包含最终应产出结果的生产者代码。
它的参数 resolve 和 reject 是由 JavaScript 自身提供的回调。我们的代码仅在 executor 的内部。
由 new Promise 构造器返回的 promise 对象具有以下内部属性:

  • state — 最初是 "pending",然后在 resolve 被调用时变为 "fulfilled",或者在 reject 被调用时变为 "rejected"。
  • result — 最初是 undefined,然后在 resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error。

executor 只能调用一个 resolve 或一个 reject。任何状态的更改都是最终的。
所有其他的再对 resolve 和 reject 的调用都会被忽略:

let promise = new Promise(function(resolve, reject) {
  resolve('done');

  reject(new Error('…')); // 被忽略
  setTimeout(() => resolve('…')); // 被忽略
});

消费函数:then,catch,finally

Promise 对象的 state 和 result 属性都是内部的。我们无法直接访问它们。但我们可以对它们使用 .then/.catch/.finally 方法。

then

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

.then 的第一个参数是一个函数,该函数将在 promise resolved 后运行并接收结果。
.then 的第二个参数也是一个函数,该函数将在 promise rejected 后运行并接收 error。

catch
如果我们只对 error 感兴趣,那么我们可以使用 null 作为第一个参数:.then(null, errorHandlingFunction)。或者我们也可以使用 .catch(errorHandlingFunction)
.catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个简写形式。

finally
finally 总是在 promise 被 settled 时运行:即 promise 被 resolve 或 reject。
finally 是执行清理(cleanup)的很好的处理程序(handler),例如无论结果如何,都停止使用不再需要的加载指示符(indicator)。

new Promise((resolve, reject) => {
  /* 做一些需要时间的事儿,然后调用 resolve/reject */
})
  // 在 promise 为 settled 时运行,无论成功与否
  .finally(() => stop loading indicator)
  // 所以,加载指示器(loading indicator)始终会在我们处理结果/错误之前停止
  .then(result => show result, err => show error)

Promise 链

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

    alert(result); // 1
    return result * 2;
  })
  .then(function(result) {
    // (***)

    alert(result); // 2
    return result * 2;
  });

返回 promise

.then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise。
在这种情况下,其他的处理程序(handler)将等待它 settled 后再获得其结果(result)。

new Promise(function(resolve, reject) {

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

}).then(function(result) {

  alert(result); // 1

  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  });

})then(function(result) {

  alert(result); // 4

});

返回 promise 使我们能够构建异步行为链。

loadScript('/article/promise-chaining/one.js')
  .then(script => loadScript('/article/promise-chaining/two.js'))
  .then(script => loadScript('/article/promise-chaining/three.js'))
  .then(script => {
    // 脚本加载完成,我们可以在这儿使用脚本中声明的函数
    one();
    two();
    three();
  });

我们可以向链中添加更多的异步行为(action)。请注意,代码仍然是“扁平”的 — 它向下增长,而不是向右。这里没有“厄运金字塔”的迹象。 Thenables
确切地说,处理程序(handler)返回的不完全是一个 promise,而是返回的被称为 “thenable” 对象 — 一个具有方法 .then 的任意对象。它会被当做一个 promise 来对待。

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { native code }
    // 1 秒后使用 this.num*2 进行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise(resolve => resolve(1))
  .then(result => {
    return new Thenable(result); // (*)
  })
  .then(alert); // 1000ms 后显示 2

这个特性允许我们将自定义的对象与 promise 链集成在一起,而不必继承自 Promise。

使用 promise 进行错误处理

Promise 链在错误(error)处理中十分强大。当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序(handler)。

fetch('https://no-such-server.blabla') // rejects
  .then(response => response.json())
  .catch(err => alert(err)); // TypeError: failed to fetch

.catch 不必是立即的。它可能在一个或多个 .then 之后出现。
隐式 try…catch
Promise 的执行者(executor)和 promise 的处理程序(handler)周围有一个“隐式的 try..catch”。如果发生异常,它就会被捕获,并被视为 rejection 进行处理。

  • 最后的 .catch 不仅会捕获显式的 rejection,还会捕获它上面的处理程序(handler)中意外出现的 error。
  • 在任何情况下我们都应该有 unhandledrejection 事件处理程序(用于浏览器,以及其他环境的模拟),以跟踪未处理的 error 并告知用户(可能还有我们的服务器)有关信息,以使我们的应用程序永远不会“死掉”。

任务
你怎么看?.catch 会被触发么?

new Promise(function(resolve, reject) {
  setTimeout(() => {
    throw new Error('Whoops!');
  }, 1000);
}).catch(alert);

Promise API

Promise.all
假设我们希望并行执行多个 promise,并等待所有 promise 都准备就绪。
例如,下面的 Promise.all 在 3 秒之后被 settled,然后它的结果就是一个 [1, 2, 3] 数组:

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(alert);

请注意,结果数组中元素的顺序与其在源 promise 中的顺序相同。即使第一个 promise 花费了最长的时间才 resolve,但它仍是结果数组中的第一个。

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

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

  • {status:"fulfilled", value:result} 对于成功的响应
  • {status:"rejected", reason:error} 对于 error。

Promise.race
与 Promise.all 类似,但只等待第一个 settled 的 promise 并获取其结果(或 error)。

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(alert); // 1

这里第一个 promise 最快,所以它变成了结果。第一个 settled 的 promise “赢得了比赛”之后,所有进一步的 result/error 都会被忽略。
Promise.resolve/reject
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));

一个题考察对 Promise 的掌握情况
问,以下代码输出什么?

Promise.resolve(x).then(y => console.log(x === y));

微任务(Microtask)

Promise 的处理程序(handlers).then、.catch 和 .finally 都是异步的。
即便一个 promise 立即被 resolve,.then、.catch 和 .finally 下面 的代码也会在这些处理程序(handler)之前被执行。

let promise = Promise.resolve();

promise.then(() => alert('promise done!'));

alert('code finished'); // 这个 alert 先显示

微任务队列(Microtask queue)

  • 队列(queue)是先进先出的:首先进入队列的任务会首先运行。
  • 只有在 JavaScript 引擎中没有其它任务在运行时,才开始执行任务队列中的任务。

当一个 promise 准备就绪时,它的 .then/catch/finally 处理程序(handler)就会被放入队列中:但是它们不会立即被执行。当 JavaScript 引擎执行完当前的代码,它会从队列中获取任务并执行它。

未处理的 rejection

如果一个 promise 的 error 未被在微任务队列的末尾进行处理,则会出现“未处理的 rejection”。

let promise = Promise.reject(new Error('Promise Failed!'));

// Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

但是如果我们忘记添加 .catch,那么,微任务队列清空后,JavaScript 引擎会触发下面这事件:

let promise = Promise.reject(new Error('Promise Failed!'));

// Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

如果我们迟一点再处理这个 error 会怎样?例如:

let promise = Promise.reject(new Error('Promise Failed!'));
setTimeout(() => promise.catch(err => alert('caught')), 1000);

// Error: Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

为什么 unhandledrejection 处理程序(handler)会运行?我们已经捕获(catch)并处理了 error!
被添加到 setTimeout 中的 .catch 也会被触发。只是会在 unhandledrejection 事件出现之后才会被触发,所以它并没有改变什么(没有发挥作用)。

Async/await

Async function

async 可以被放置在一个函数前面,即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。

async function f() {
  return 1;
}

f().then(alert); // 1

Await

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

async function f() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('done!'), 1000);
  });

  let result = await promise; // 等待,直到 promise resolve (*)

  alert(result); // "done!"
}

f();

await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。
相比于 promise.then,它只是获取 promise 的结果的一个更优雅的语法,同时也更易于读写。 await 接受 “thenables”

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // 1000ms 后使用 this.num*2 进行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
}

async function f() {
  // 等待 1 秒,之后 result 变为 2
  let result = await new Thenable(1);
  alert(result);
}

f();