Open jannahuang opened 2 years ago
假设有个异步操作:
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 对象的构造器(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。
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) 的完全的模拟,它只是一个简写形式,是一个语法糖。
promise.finally( function() { /* handle finally */ } );
无论 promise 被 resolve 还是 reject,都会调用 .finally。.finally 可以避免 .then 和 .catch 处理程序中出 现冗余代码。但是 .finally 无法知道 promise 状态,主要用于添加清理代码。 finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。
因为 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; });
.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 被 reject 时,控制权将移交至最近的 rejection 处理程序。 .catch 不必是立即的。可以有许多个 .then 处理程序,然后在尾端使用一个 .catch 处理上面的所有 error。
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 处理程序。
如果出现 error,promise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序。 但如果没有相应的处理程序,JavaScript 引擎会跟踪此类 rejection,会生成一个全局的 error,脚本会报错,并在控制台留下信息。 在浏览器中,我们可以使用 unhandledrejection 事件来捕获这类 error:
window.addEventListener('unhandledrejection', function(event) { // 这个事件对象有两个特殊的属性: alert(event.promise); // 生成该全局 error 的 promise alert(event.reason); // 未处理的 error 对象 });
new Promise(function(resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert);
不会触发。 函数代码周围有个“隐式的 try..catch”。所以,所有同步错误都会得到处理。 但是这里的错误并不是在 executor 运行时生成的,而是在 setTimout 稍后生成的。因此,promise 无法处理它。
在 Promise 类中,有 6 种静态方法。其中最常用的是 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 都被 settle,无论结果如何都返回结果数组。 结果数组包括:
[ {status: 'fulfilled', value: ...response...}, {status: 'fulfilled', value: ...response...}, {status: 'rejected', reason: ...error object...} ]
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 只等待第一个 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(value) 用结果 value 创建一个 resolved 的 promise。 效果等同于:
let promise = new Promise(resolve => resolve(value));
Promise.reject(error) 用 error 创建一个 rejected 的 promise。 效果等同于:
let promise = new Promise((resolve, reject) => reject(error));
实际上,这个方法几乎从未被使用过。(谁没事会创建被 reject 的 promise...)
async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。
在函数前面的 “async” 表示:这个函数总是返回一个 promise。其他内容将自动被包装在一个 resolved 的 promise 中。
async function f() { return 1; } // 上述代码与下述代码效果相同 async function f() { return Promise.resolve(1); }
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), ... ]);
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
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 文档
Promise
以往的异步编程模式
假设有个异步操作:
如果在后续代码需要使用x,异步执行的函数需要在更新 x 的值以后通知其他代码。 在早期的JavaScript 中,只支持定义回调函数来表明异步操作完成。
异步返回值
解决方法是给异步操作提供一个回调,异步返回值可作为参数传给回调函数。
失败处理 分别处理成功回调和失败回调:
当有多个异步操作时,通常需要深度嵌套的回调函数(俗称“回调地狱”)来解决。随着代码越来越复杂,回调策略是不具有扩展性的。因此,为了解决异步问题,出现了 Promise。
Promise
Promise 对象的构造器(constructor)语法如下:
传递给 new Promise 的函数被称为 executor。它的参数 resolve 和 reject 是由 JavaScript 自身提供的回调。当 new Promise 被创建,executor 会自动运行。(事件循环考点)
当 executor 有结果时,它应该调用以下回调之一:
也就是 executor 会自动运行并尝试执行一项工作。尝试结束后,如果成功则调用 resolve,如果出现 error 则调用 reject。
Promise.resolve() promise 并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用 Promise.resolve() 静态方法,可以实例化一个解决的期约。下面两个期约实例实际上是一样的:
Promise.reject() 与 Promise.resolve() 类似,Promise.reject()会 实例化一个rejected promise 并抛出一个异步错误(这个错误不能通过try/catch 捕获,而只能通过拒绝处理程序 .then(null, f) 或 .catch(f) 捕获)。下面的两个期约实例实际上是 一样的:
由 new Promise 构造器返回的 promise 对象具有以下内部属性:
executor 只能调用一个 resolve 或一个 reject,状态改变后不可再更改。 并且,resolve/reject 只需要一个参数(或不包含任何参数),并且将忽略额外的参数。也可以立即调用 resolve 或 reject。
then, catch
.then 的第一个参数是一个函数,该函数将在 promise resolved 且接收到结果后执行。 .then 的第二个参数也是一个函数,该函数将在 promise rejected 且接收到 error 信息后执行。 如果我们只关心 resolved 结果,可以只给 .then 提供一个参数。
如果我们只关心 rejected 结果,可以使用 .then(null, errorHandlingFunction),或者 .catch(errorHandlingFunction)。 .catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个简写形式,是一个语法糖。
finally
无论 promise 被 resolve 还是 reject,都会调用 .finally。.finally 可以避免 .then 和 .catch 处理程序中出 现冗余代码。但是 .finally 无法知道 promise 状态,主要用于添加清理代码。 finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。
Promise 链
因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回的是 promise,所以它们可以被链式调用。
但是单独多次添加到一个 promise 上,并不是 promise 链。它们不会相互传递 result,而是彼此独立运行处理任务。
返回 promise
.then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise。其他的处理程序将等待它 settled 后再获得其结果。当它被 settled 后,其 result(或 error)将被进一步传递下去。
使用 promise 进行错误处理
当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序。 .catch 不必是立即的。可以有许多个 .then 处理程序,然后在尾端使用一个 .catch 处理上面的所有 error。
隐式 try...catch
promise 的 executor 和处理程序周围有一个“隐式的 try..catch”。如果发生异常,它就会被捕获,并被视为 rejection 进行处理。
在处理程序中也一样。比如在 .then 处理程序中 throw,这意味着 promise rejected,因此控制权移交至最近的 error 处理程序。
再次抛出错误
如果在 .catch 中无法处理该 error,可以再次抛出错误 throw,那么控制权就会被移交到下一个最近的 error 处理程序。 如果处理该 error 并正常完成,那么它将继续到最近的成功的 .then 处理程序。
未处理的 rejection
如果出现 error,promise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序。 但如果没有相应的处理程序,JavaScript 引擎会跟踪此类 rejection,会生成一个全局的 error,脚本会报错,并在控制台留下信息。 在浏览器中,我们可以使用 unhandledrejection 事件来捕获这类 error:
练习
不会触发。 函数代码周围有个“隐式的 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 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error。
如果其中一个 promise 被 reject,Promise.all 就会立即被 reject,完全忽略列表中其他的 promise。它们的结果也被忽略。
其他情况:
Promise.allSettled(新增的特性)
Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何都返回结果数组。 结果数组包括:
Promise.race
Promise.race 只等待第一个 settled 的 promise 并获取其结果(或 error),其他的 settled 将被忽略。
Promise.any
Promise.any 只等待第一个 fulfilled 的 promise,并将这个 fulfilled 的 promise 返回。 如果给出的 promise 都 rejected,那么返回的 promise 会带有 AggregateError —— 一个特殊的 error 对象,在其 errors 属性中存储着所有 promise error。
Promise.resolve
Promise.resolve(value) 用结果 value 创建一个 resolved 的 promise。 效果等同于:
Promise.reject
Promise.reject(error) 用 error 创建一个 rejected 的 promise。 效果等同于:
实际上,这个方法几乎从未被使用过。(谁没事会创建被 reject 的 promise...)
async/await
async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。
async function
在函数前面的 “async” 表示:这个函数总是返回一个 promise。其他内容将自动被包装在一个 resolved 的 promise 中。
await
await 只在 async 函数内工作。关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。
await 暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。
async/await 和 promise.then/catch 当使用 async/await 时,几乎就不会用到 .then 了,因为 await 处理了等待。并处理 error 会使用常规的 try..catch 而不是 .catch。这通常(但不总是)更加方便。
async/await 可以和 Promise.all 一起使用
练习
loadJson('https://javascript.info/no-such-user.json') .catch(alert); // Error: 404
function f() { // ……这里你应该怎么写? // 我们需要调用 async wait() 并等待以拿到结果 10 // 记住,我们不能使用 "await" }
只需要把 async 调用当作 promise 对待,并在它的后面加上 .then 即。
以上笔记参考《现代 JavaScript 教程》,《JavaScript高级程序设计(第4版)》及 MDN 文档