简介:回调
JavaScript 主机(host)环境提供了许多函数,这些函数允许我们计划 异步 行为(action)。换句话说,我们现在开始执行的行为,但它们会在稍后完成。
例如,setTimeout 函数就是一个这样的函数。
脚本是“异步”调用的,因为它从现在开始加载,但是在这个加载函数执行完成后才运行。
如果在 loadScript(…) 下面有任何其他代码,它们不会等到脚本加载完成才执行。
loadScript('/my/script.js');
// loadScript 下面的代码
// 不会等到脚本加载完成才执行
// ...
假设我们需要在新脚本加载后立即使用它。它声明了新函数,我们想运行它们。
但如果我们在 loadScript(…) 调用后立即执行此操作,这将不会有效。
loadScript('/my/script.js'); // 这个脚本有 "function newFunction() {…}"
newFunction(); // 没有这个函数!
自然情况下,浏览器可能没有时间加载脚本。
让我们添加一个 callback 函数作为 loadScript 的第二个参数,该函数应在脚本加载完成时执行:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
现在,如果我们想调用该脚本中的新函数,我们应该将其写在回调函数中:
loadScript('/my/script.js', function() {
// 在脚本加载完成后,回调函数才会执行
newFunction(); // 现在它工作了
...
});
这是我们的想法:第二个参数是一个函数(通常是匿名函数),该函数会在行为(action)完成时运行。
Promise
Promise 是将“生产者代码”和“消费者代码”连接在一起的一个特殊的 JavaScript 对象。
Promise 对象的构造器(constructor)语法如下
let promise = new Promise(function(resolve, reject) {
// executor(生产者代码,“歌手”)
});
传递给 new Promise 的函数被称为 executor。当 new Promise 被创建,executor 会自动运行。它包含最终应产出结果的生产者代码。按照上面的类比:executor 就是“歌手”。
当 executor 获得了结果,无论是早还是晚都没关系,它应该调用以下回调之一:
- resolve(value) — 如果任务成功完成并带有结果 value。
- reject(error) — 如果出现了 error,error 即为 error 对象。
所以总结一下就是:executor 会自动运行并尝试执行一项工作。尝试结束后,如果成功则调用 resolve,如果出现 error 则调用 reject。
消费者:then,catch,finally
Promise 对象充当的是 executor(“生产者代码”或“歌手”)和消费函数(“粉丝”)之间的连接,后者将接收结果或 error。可以通过使用 .then、.catch 和 .finally 方法为消费函数进行注册。
then
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);
catch
finally
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;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
它的理念是将 result 通过 .then 处理程序(handler)链进行传递。
返回 promise
.then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise。
更复杂的示例:fetch
在前端编程中,promise 通常被用于网络请求。那么,让我们一起来看一个相关的扩展示例吧。
我们将使用 fetch 方法从远程服务器加载用户信息。它有很多可选的参数,我们在 单独的一章 中对其进行了详细介绍,但是基本语法很简单:
let promise = fetch(url);
执行这条语句,向 url 发出网络请求并返回一个 promise。当远程服务器返回 header(是在 全部响应加载完成前)时,该 promise 使用一个 response 对象来进行 resolve。
下面这段代码向 user.json 发送请求,并从服务器加载该文本:
fetch('/article/promise-chaining/user.json')
// 当远程服务器响应时,下面的 .then 开始执行
.then(function(response) {
// 当 user.json 加载完成时,response.text() 会返回一个新的 promise
// 该 promise 以加载的 user.json 为 result 进行 resolve
return response.text();
})
.then(function(text) {
// ...这是远程文件的内容
alert(text); // {"name": "iliakan", "isAdmin": true}
});
为了简洁,我们还将使用箭头函数:
// 同上,但是使用 response.json() 将远程内容解析为 JSON
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => alert(user.name)); // iliakan, got user name
使用 promise 进行错误处理
Promise API
在 Promise 类中,有 5 种静态方法。我们在这里简单介绍下它们的使用场景。
Promise.all
假设我们希望并行执行多个 promise,并等待所有 promise 都准备就绪。如果任意的 promise reject,则 Promise.all 整个将会 reject。
let promise = Promise.all([...promises...]);
例如,下面的 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); // 1,2,3 当上面这些 promise 准备好时:每个 promise 都贡献了数组中的一个元素
Promise.allSettled
Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何。
Promise.race
与 Promise.all 类似,但只等待第一个 settled 的 promise 并获取其结果(或 error)。
Promise.resolve/reject
在现代的代码中,很少需要使用 Promise.resolve 和 Promise.reject 方法,因为 async/await 语法(我们会在 稍后 讲到)使它们变得有些过时了。
Promisification
它指将一个接受回调的函数转换为一个返回 promise 的函数。
由于许多函数和库都是基于回调的,因此,在实际开发中经常会需要进行这种转换。
微任务(Microtask)
Promise 的处理程序(handlers).then、.catch 和 .finally 都是异步的。
即便一个 promise 立即被 resolve,.then、.catch 和 .finally 下面 的代码也会在这些处理程序(handler)之前被执行。
微任务队列(Microtask queue)
简单地说,当一个 promise 准备就绪时,它的 .then/catch/finally 处理程序(handler)就会被放入队列中:但是它们不会立即被执行。当 JavaScript 引擎执行完当前的代码,它会从队列中获取任务并执行它。
Promise 的处理程序(handler)总是会经过这个内部队列。
未处理的 rejection
总结
Promise 处理始终是异步的,因为所有 promise 行为都会通过内部的 “promise jobs” 队列,也被称为“微任务队列”(ES8 术语)。
因此,.then/catch/finally 处理程序(handler)总是在当前代码完成后才会被调用。
如果我们需要确保一段代码在 .then/catch/finally 之后被执行,我们可以将它添加到链式调用的 .then中。
Async/await
Async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。
Async function
让我们以 async 这个关键字开始。它可以被放置在一个函数前面,如下所示:
async function f() {
return 1;
}
在函数前面的 “async” 这个单词表达了一个简单的事情:即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。
例如,下面这个函数返回一个结果为 1 的 resolved promise,让我们测试一下:
async function f() {
return 1;
}
f().then(alert); // 1
……我们也可以显式地返回一个 promise,结果是一样的:
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
所以说,async 确保了函数返回一个 promise,也会将非 promise 的值包装进去。很简单,对吧?但不仅仅这些。还有另外一个叫 await 的关键词,它只在 async 函数内工作,也非常酷。
Await
// 只在 async 函数内工作
let value = await promise;
关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。
这里的例子就是一个 1 秒后 resolve 的 promise:
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // 等待,直到 promise resolve (*)
alert(result); // "done!"
}
f();
这个函数在执行的时候,“暂停”在了 () 那一行,并在 promise settle 时,拿到 result 作为结果继续往下执行。所以上面这段代码在一秒后显示 “done!”。
让我们强调一下:await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。
相比于 promise.then,它只是获取 promise 的结果的一个更优雅的语法,同时也更易于读写。
不能在普通函数中使用 await
*await 不能在顶层代码运行
// 用在顶层代码中会报语法错误
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
但我们可以将其包裹在一个匿名 async 函数中,如下所示:
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
async/await 和 promise.then/catch
当我们使用 async/await 时,几乎就不会用到 .then 了,因为 await 为我们处理了等待。并且我们使用常规的 try..catch 而不是 .catch。