简介:回调
JavaScript 主机(host)环境提供了许多函数,这些函数允许我们计划 异步 行为(action)—— 也就是在我们执行一段时间后才自行完成的行为。
- onload:通常会在脚本加载和执行完成后执行一个函数。
- 在回调中回调
- 处理 Error
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script); //成功
script.onerror = () => callback(new Error(`Script load error for ${src}`)); //失败
document.head.append(script);
}
- 厄运金字塔
- 随着调用嵌套的增加,代码层次变得更深,维护难度也随之增加
Promise
“生产者代码(producing code)”会做一些事儿,并且会需要一些时间。例如,通过网络加载数据的代码。它就像一位“歌手”。
“消费者代码(consuming code)”想要在“生产者代码”完成工作的第一时间就能获得其工作成果。许多函数可能都需要这个结果。这些就是“粉丝”。
Promise 是将“生产者代码”和“消费者代码”连接在一起的一个特殊的 JavaScript 对象。用我们的类比来说:这就是就像是“订阅列表”。“生产者代码”花费它所需的任意长度时间来产出所承诺的结果,而 “promise” 将在它(译注:指的是“生产者代码”,也就是下文所说的 executor)准备好时,将结果向所有订阅了的代码开放。
Promise 对象的构造器(constructor)语法
let promise = new Promise(function(resolve,reject){
//生产者代码
})
//当 `new Promise` 被创建,executor 会自动运行。
- `resolve(value)` —— 如果任务成功完成并带有结果 `value`。
- `reject(error)` —— 如果出现了 error,`error` 即为 error 对象。
只有一个结果或一个 error
executor 只能调用一个 `resolve` 或一个 `reject`。任何状态的更改都是最终的。
以 `Error` 对象 reject:如果什么东西出了问题,executor 应该调用 `reject`。
executor 通常是异步执行某些操作,并在一段时间后调用 `resolve/reject`,但这不是必须的。我们还可以立即调用 `resolve` 或 `reject`。
`state` 和 `result` 都是内部的
Promise 对象的 `state` 和 `result` 属性都是内部的。我们无法直接访问它们。
- 消费者:then,catch
- Promise 对象充当的是 executor(“生产者代码”)和消费函数之间的连接,后者将接收结果或 error。可以通过使用
.then
和.catch
方法注册消费函数。 - then
.then
的第一个参数是一个函数,该函数将在 promise resolved 且接收到结果后执行。.then
的第二个参数也是一个函数,该函数将在 promise rejected 且接收到 error 信息后执行。
- catch
- 如果我们只对 error 感兴趣,那么我们可以使用
null
作为第一个参数:.then(null, errorHandlingFunction)
。或者我们也可以使用.catch(errorHandlingFunction)
,其实是一样的
- 如果我们只对 error 感兴趣,那么我们可以使用
- Promise 对象充当的是 executor(“生产者代码”)和消费函数之间的连接,后者将接收结果或 error。可以通过使用
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) 与 promise.then(null, f) 一样
promise.catch(alert); // 1 秒后显示 "Error: Whoops!"
- 清理:finally
- 调用
.finally(f)
类似于.then(f, f)
,因为当 promise settled 时f
就会执行:无论 promise 被 resolve 还是 reject。 finally
的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。finally
处理程序(handler)没有参数。在finally
中,我们不知道 promise 是否成功。没关系,因为我们的任务通常是执行“常规”的完成程序(finalizing procedures)。finally
处理程序将结果或 error “传递”给下一个合适的处理程序。finally
处理程序也不应该返回任何内容。如果它返回了,返回的值会默认被忽略。- 当
finally
抛出 error 时,执行将转到最近的 error 的处理程序。
- 当
- 我们可以对 settled 的 promise 附加处理程序
- 如果 promise 为 pending 状态,
.then/catch/finally
处理程序(handler)将等待它的结果。 - 当我们向一个 promise 添加处理程序时,它可能已经 settled 了
- 如果 promise 为 pending 状态,
- 调用
- 示例:loadScript
//基本回调函数
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
//用 promise 重写
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);
});
}
//用法
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('Another handler...'));
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;
});
运行流程:
1. 初始 promise 在 1 秒后 resolve `(*)`,
2. 然后 `.then` 处理程序被调用 `(**)`,它又创建了一个新的 promise(以 `2` 作为值 resolve)。
3. 下一个 `then` `(***)` 得到了前一个 `then` 的值,对该值进行处理(*2)并将其传递给下一个处理程序。
4. 以此类推
新手常犯的一个经典错误:从技术上讲,我们也可以将多个 .then
添加到一个 promise 上。但这并不是 promise 链(chaining)。
- 返回 promise
.then(handler)
中所使用的处理程序(handler)可以创建并返回一个 promise。
- 示例:loadScript
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();
});
处理程序返回的不完全是一个 promise,而是返回的被称为 “thenable” 对象 —— 一个具有方法 `.then` 的任意对象。它会被当做一个 promise 来对待。
- 更复杂的示例:fetch
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}
});
//加载用户个人资料显示头像
// 发送一个对 user.json 的请求
fetch('/article/promise-chaining/user.json')
// 将其加载为 JSON
.then(response => response.json())
// 发送一个到 GitHub 的请求
.then(user => fetch(`https://api.github.com/users/${user.name}`))
// 将响应加载为 JSON
.then(response => response.json())
// 显示头像图片(githubUser.avatar_url)3 秒(也可以加上动画效果)
.then(githubUser => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => img.remove(), 3000); // (*)
});
使用 promise 进行错误处理
当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序。
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));
隐式 try…catch promise 的执行者(executor)和 promise 的处理程序周围有一个“隐式的 try..catch
”。如果发生异常,它就会被捕获,并被视为 rejection 进行处理。
再次抛出(Rethrowing).catch
块正常完成。所以下一个成功的 .then
处理程序就会被调用。
未处理的 rejection 如果出现 error,promise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序。 但是如果error没有程序去处理,脚本就会死掉。 JavaScript 引擎会跟踪此类 rejection,在这种情况下会生成一个全局的 error。
Promise API
在 Promise
类中,有 6 种静态方法。
- Promise.all
- let promise = Promise.all(iterable);
- 希望并行执行多个 promise,并等待所有 promise 都准备就绪。
Promise.all
接受一个可迭代对象(通常是一个数组项为 promise 的数组),并返回一个新的 promise。- 如果任意一个 promise 被 reject,由
Promise.all
返回的 promise 就会立即 reject,并且带有的就是这个 error。 Promise.all(iterable)
允许在iterable
中使用非 promise 的“常规”值
- Promise.allSettled
Promise.allSettled
等待所有的 promise 都被 settle,无论结果如何。结果数组会是这样的- 对成功的响应,结果数组对应元素的内容为
{status:"fulfilled", value:result}
, - 对出现 error 的响应,结果数组对应元素的内容为
{status:"rejected", reason:error}
。
- 对成功的响应,结果数组对应元素的内容为
- Polyfill
- 如果浏览器不支持
Promise.allSettled
,很容易进行 polyfill - 这个处理程序将成功的结果
value
转换为{status:'fulfilled', value}
,将 errorreason
转换为{status:'rejected', reason}
。这正是Promise.allSettled
的格式。 - 我们就可以使用
Promise.allSettled
来获取 所有 给定的 promise 的结果,即使其中一些被 reject。
- 如果浏览器不支持
- Promise.race
- 与
Promise.all
类似,但只等待第一个 settled 的 promise 并获取其结果(或 error)。 - let promise = Promise.race(iterable);
- 与
- Promise.any
- 1与
Promise.race
类似,区别在于Promise.any
只等待第一个 fulfilled 的 promise,并将这个 fulfilled 的 promise 返回。 - let promise = Promise.any(iterable);
- 1与
- Promise.resolve/reject
- 很少需要使用
Promise.resolve
和Promise.reject
方法,因为async/await
语法- 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));
- Promise.resolve
- 很少需要使用
Promisification
将一个接受回调的函数转换为一个返回 promise 的函数。
function promisify(f) {
return function (...args) { // 返回一个包装函数(wrapper-function) (*)
return new Promise((resolve, reject) => {
function callback(err, result) { // 我们对 f 的自定义的回调 (**)
if (err) {
reject(err);
} else {
resolve(result);
}
}
args.push(callback); // 将我们的自定义的回调附加到 f 参数(arguments)的末尾
f.call(this, ...args); // 调用原始的函数
});
};
}
// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
微任务(Microtask)
promise 的处理程序 .then
、.catch
和 .finally
都是异步的
- 微任务队列(Microtask queue)
- 异步任务需要适当的管理。为此,ECMA 标准规定了一个内部队列
PromiseJobs
,通常被称为“微任务队列(microtask queue)”(V8 术语)。- 队列(queue)是先进先出的:首先进入队列的任务会首先运行。
- 只有在 JavaScript 引擎中没有其它任务在运行时,才开始执行任务队列中的任务。
- 如果执行顺序对我们很重要该怎么办?我们怎么才能让
code finished
在promise done
之后出现呢?- 使用
.then
将其放入队列
- 使用
- 异步任务需要适当的管理。为此,ECMA 标准规定了一个内部队列
- 未处理的 rejection
- 如果一个 promise 的 error 未被在微任务队列的末尾进行处理,则会出现“未处理的 rejection”。
async、await
- async function
async function f() {
return 1;
}
即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。
- await
// 只在 async 函数内工作
let value = await promise;
1. 关键字 `await` 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。
2. 不能在普通函数中使用 `await`
3. 现代浏览器在 modules 里允许顶层的 `await`
4. `await` 接受 “thenables”
5. `async/await` 可以和 `Promise.all` 一起使用
6. Class 中的 async 方法
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1(alert 等同于 result => alert(result))
- Error 处理
async function f() {
await Promise.reject(new Error("Whoops!"));
}
//实际开发中
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// 捕获到 fetch 和 response.json 中的错误
alert(err);
}
}
f();