Promise 必知必会
回调#
🚩 异步 行为(action):现在开始执行的行为,但它们会在稍后完成(例如,setTimeout 函数就是一个这样的函数;例如加载脚本和模块)
实际中的异步行为的示例:
/**
* 使用给定的 src 加载脚本
* @param src
**/
function loadScript(src) {
// 创建一个 <script> 标签,并将其附加到页面
// 这将使得具有给定 src 的脚本开始加载,并在加载完成后运行
let script = document.createElement("script");
script.src = src;
document.head.append(script);
}
可以像这样使用这个函数:
// 在给定路径下加载并执行脚本
loadScript("/my/script.js");
// loadScript 下面的代码
// 不会等到脚本加载完成才执行
// ...
// 💡脚本是“异步”调用的,因为它从现在开始加载,但是在这个加载函数执行完成后才运行。如果在 loadScript(…) 下面有任何其他代码,它们不会等到脚本加载完成才执行
假设需要在新脚本加载后立即使用它,这将不会有效:
loadScript("/my/script.js"); // 这个脚本有 "function newFunction() {…}"
newFunction(); // 没有这个函数!
😭 到目前为止,loadScript 函数并没有提供跟踪加载完成的方法。脚本加载并最终运行,仅此而已。但是希望了解脚本何时加载完成,以使用其中的新函数和变量
💡 添加一个 callback 函数作为 loadScript 的第二个参数,该函数应在脚本加载完成时执行:
function loadScript(src, callback) {
let script = document.createElement("script");
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
loadScript(
"https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js",
(script) => {
// 在脚本加载完成后,回调函数才会执行
alert(`Cool, the script ${script.src} is loaded`);
alert(_); // 所加载的脚本中声明的函数
}
);
''''这就是被称为“基于回调”的异步编程风格''
'':异步执行某项功能的函数应该提供一个 callback 参数用于在相应事件完成时调用
🚩 回调地狱
如何依次加载两个脚本:第一个,然后是第二个?第三个?
loadScript("/my/script.js", function (script) {
loadScript("/my/script2.js", function (script) {
loadScript("/my/script3.js", function (script) {
// ...加载完所有脚本后继续
});
});
});
加入处理 Error:
loadScript("1.js", function (error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript("2.js", function (error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript("3.js", function (error, script) {
if (error) {
handleError(error);
} else {
// ...加载完所有脚本后继续 (*)
}
});
}
});
}
});
这就是著名的“''回调地狱''
”或“厄运金字塔”
💡 可以通过使每个行为都成为一个独立的函数来尝试减轻这种问题
loadScript("1.js", step1);
function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript("2.js", step2);
}
}
function step2(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript("3.js", step3);
}
}
function step3(error, script) {
if (error) {
handleError(error);
} else {
// ...加载完所有脚本后继续 (*)
}
}
优缺点
- 没有深层的嵌套,独立为顶层函数
- 可读性差
- 没有重用
最好的方法之一就是 “''promise''
”
Promise#
🚩 语法
let promise = new Promise(function (resolve, reject) {
// executor
// 当 promise 被构造完成时,executor自动执行此函数
// executor 通常是异步任务
// ...
})
// handler
.then(
(result) => {
// ...
},
(error) => {
// ...
}
);
1.当 new Promise 被创建,executor 被自动且立即调用
2.由 new Promise 构造器返回的 promise 对象具有以下【内部属性】
- state — 最初是 "pending",然后在 resolve 被调用时变为 "fulfilled",或者在 reject 被调用时变为 "rejected"
- result — 最初是 undefined,然后在 resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error
3.与最初的 “pending” promise 相反,一个 resolved 或 rejected 的 promise 都会被称为 “settled”
4.executor 只能调用一个 resolve 或一个 reject;任何状态的更改都是最终的(不可逆)
🚩 立即 resolve/reject 的 Promise
// executor 通常是异步执行某些操作,并在一段时间后调用 resolve/reject,但这不是必须的;还可以立即调用 resolve 或 reject
// 💡当开始做一个任务时,但随后看到一切都已经完成并已被缓存时,可能就会发生这种情况。这挺好😀
let promise = new Promise(function (resolve, reject) {
// 不花时间去做这项工作
resolve(123); // 立即给出结果:123
});
🚩 示例:加载脚本的 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);
}
// 用法:
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
// 在脚本加载完成后,回调函数才会执行
alert(`${script.src} is loaded!`);
alert( _ ); // 所加载的脚本中声明的函数
});
基于 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 链#
🚩Promise 链:回忆回调中,何依次加载两个脚本:第一个,然后是第二个?第三个?
// 💡Promise 提供了一些方案来做到这一点:Promise 链
// like this
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;
});
// 📌为什么可以?因为对 promise.then 的调用会返回了一个 promise,所以我们可以在其之上调用下一个 .then
// 当处理程序(handler)返回一个值时,它将成为该 promise 的 result,所以将使用它调用下一个 .then
// 💣''新手常犯的一个经典错误:从技术上讲,我们也可以将多个 .then 添加到一个 promise 上。但这并不是 promise 链(chaining)''
let promise = new Promise(function (resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function (result) {
alert(result); // 1
return result * 2;
});
promise.then(function (result) {
alert(result); // 1
return result * 2;
});
promise.then(function (result) {
alert(result); // 1
return result * 2;
});
// 💡这里所做的只是一个 promise 的几个处理程序(handler)。它们不会相互传递 result;相反,它们之间彼此独立运行处理任务
🚩 返回 promise
- .then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise
- 在这种情况下,其他的处理程序(handler)将【等待它 settled 后再获得其结果(result)】
示例: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();
});
// 💡注意:这儿每个 loadScript 调用都返回一个 promise,并且在它 resolve 时下一个 .then 开始运行。然后,它启动下一个脚本的加载。所以,脚本是一个接一个地加载的
// 💡并且代码仍然是“扁平”的 — 它向下增长,而不是向右
// ...
// 从技术上讲,可以向每个 loadScript 直接添加 .then,就像这样:
loadScript("/article/promise-chaining/one.js").then((script1) => {
loadScript("/article/promise-chaining/two.js").then((script2) => {
loadScript("/article/promise-chaining/three.js").then((script3) => {
// 此函数可以访问变量 script1,script2 和 script3
one();
two();
three();
});
});
});
// 💡这段代码做了相同的事儿:按顺序加载 3 个脚本。但它是“向右增长”的。所以会有和使用回调函数一样的问题
// 👍刚开始使用 promise 的人可能不知道 promise 链,所以他们就这样写了。通常,链式是首选
Thenables#
- 确切地说,处理程序(handler)返回的不完全是一个 promise,而是返回的被称为 “thenable” 对象 — 一个具有方法 .then 的任意对象
- thenable对象会被当做一个 promise 来对待
- 这个想法是,第三方库可以实现自己的“promise 兼容(promise-compatible)”对象;它们可以具有扩展的方法集,但也与原生的 promise 兼容,因为它们实现了 .then 方法
- 这个特性允许将自定义的对象与 promise 链集成在一起,而不必继承自 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
- 这样就可以使得之后计划后续的行为成为可能
- 即使现在不打算对链进行扩展,但之后可能会需要
示例:
function loadJson(url) {
return fetch(url).then((response) => response.json());
}
function loadGithubUser(name) {
return fetch(`https://api.github.com/users/${name}`).then((response) =>
response.json()
);
}
function showAvatar(githubUser) {
return new Promise(function (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);
});
}
// 使用它们:
loadJson("/article/promise-chaining/user.json")
.then((user) => loadGithubUser(user.name))
.then(showAvatar)
.then((githubUser) => alert(`Finished showing ${githubUser.name}`));
// ...
错误处理#
🚩Promise 链在错误(error)处理
- 当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序(handler);这在实际开发中非常方便
- .catch 不必是立即的;它可能在一个或多个 .then 之后出现
示例:
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 的处理程序(handler)周围有一个“隐式的 try..catch”
- 如果发生异常,它(译注:指异常)就会被捕获,并被视为 rejection 进行处理
示例:
// excutor 中
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!
// 等同于
new Promise((resolve, reject) => {
reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!
// ...
// handler 中
new Promise((resolve, reject) => {
resolve("ok");
})
.then((result) => {
throw new Error("Whoops!"); // reject 这个 promise
})
.catch(alert); // Error: Whoops!
🚩 再次抛出(Rethrowing)
- 如果在 .catch 中 throw,那么控制权就会被移交到下一个最近的 error 处理程序(handler)。如果处理该 error 并正常完成,那么它将继续到最近的成功的 .then 处理程序(handler)
// 执行流:catch -> then
new Promise((resolve, reject) => {
throw new Error("Whoops!");
})
.catch(function (error) {
alert("The error is handled, continue normally");
})
.then(() => alert("Next successful handler runs"));
// 执行流:catch -> catch
new Promise((resolve, reject) => {
throw new Error("Whoops!");
})
.catch(function (error) {
// (*)
if (error instanceof URIError) {
// 处理它
} else {
alert("Can't handle such error");
throw error; // 再次抛出此 error 或另外一个 error,执行将跳转至下一个 catch
}
})
.then(function () {
/* 不在这里运行 */
})
.catch((error) => {
// (**)
alert(`The unknown error has occurred: ${error}`);
// 不会返回任何内容 => 执行正常进行
});
🚩 未处理的 rejection
new Promise(function () {
noSuchFunction(); // 这里出现 error(没有这个函数)
}).then(() => {
// 一个或多个成功的 promise 处理程序(handler)
}); // 尾端没有 .catch!
// ...
// 当一个 error 没有被处理会发生什么?
// 💡如果出现 error,promise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序(handler)。但是上面这个例子中并没有这样的处理程序(handler)。因此 error 会“卡住(stuck)”。没有代码来处理它
// 在实际开发中,就像代码中常规的未处理的 error 一样,这意味着某些东西出了问题
// 当发生一个常规的错误(error)并且未被 try..catch 捕获时会发生什么?脚本死了,并在控制台(console)中留下了一个信息。对于在 promise 中未被处理的 rejection,也会发生类似的事儿
JavaScript 引擎会跟踪此类 rejection,在这种情况下会生成一个全局的 error
- 在浏览器中,可以使用 unhandledrejection 事件来捕获这类 error
window.addEventListener("unhandledrejection", function (event) {
// 这个事件对象有两个特殊的属性:
alert(event.promise); // [object Promise] - 生成该全局 error 的 promise
alert(event.reason); // Error: Whoops! - 未处理的 error 对象
});
new Promise(function () {
throw new Error("Whoops!");
}); // 没有用来处理 error 的 catch
Promise API#
在 Promise 类中,有 5 种静态方法
- Promise.all([iterable])
- Promise.allSettled([iterable])
- Promise.race([iterable])
- Promise.resolve()
- Promise.reject()
🚩Promise.all
语法
// 接受一个 promise 数组(可以是任何可迭代的)作为参数并返回一个新的 promise
let promise = Promise.all([iterable]);
注意
- 并行执行多个 promise,当所有给定的 promise 都被 成功 时,新的 promise 才会 resolve,并且其结果数组将成为新的 promise 的结果
- 结果数组中元素的顺序与其在源 promise 中的顺序相同(即使第一个 promise 花费了最长的时间)
- 如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error
🚩 如果出现 error,其他 promise 将被忽略
- 如果其中一个 promise 被 reject,Promise.all 就会立即被 reject,完全忽略列表中其他的 promise。它们的结果也被忽略
- 例如,如果有多个同时进行的 fetch 调用,其中一个失败,其他的 fetch 操作仍然会继续执行,但是 Promise.all 将不会再关心(watch)它们。它们可能会 settle,但是它们的结果将被忽略
- Promise.all 没有采取任何措施来取消它们,因为 promise 中没有“取消”的概念
🚩Promise.all(iterable) 允许在 iterable 中使用 non-promise 的“常规”值
// romise.all(...) 接受含有 promise 项的可迭代对象(大多数情况下是数组)作为参数。但是,如果这些对象中的任何一个不是 promise,那么它将被“按原样”传递给结果数组
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
}),
2,
3,
]).then(alert); // 1, 2, 3
🚩Promise.allSettled
Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何,结果数组具有:
- {status:"fulfilled", value:result} 对于成功的响应
- {status:"rejected", reason:error} 对于 error
Polyfill
if (!Promise.allSettled) {
const rejectHandler = (reason) => ({ status: "rejected", reason });
const resolveHandler = (value) => ({ status: "fulfilled", value });
Promise.allSettled = function (promises) {
const convertedPromises = promises.map((p) =>
Promise.resolve(p).then(resolveHandler, rejectHandler)
);
return Promise.all(convertedPromises);
};
}
🚩Promise.race
- 只等待第一个 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.resolve/reject
语法
// 结果 value 创建一个 resolved 的 promise
Promise.resolve(value);
// 等同于
let promise = new Promise((resolve) => resolve(value));
//...
// Promise.reject() 类似
- 当一个函数被期望返回一个 promise 时,这个方法用于兼容性
- 💡这里的兼容性是指,直接从缓存中获取了当前操作的结果 value,但是期望返回的是一个 promise,所以可以使用 Promise.resolve(value) 将 value “封装”进 promise,以满足期望返回一个 promise 的这个需求
示例:
let cache = new Map();
function loadCached(url) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // (*)
}
return fetch(url)
.then((response) => response.text())
.then((text) => {
cache.set(url, text);
return text;
});
}
// 💡可以使用 loadCached(url).then(…),因为该函数保证了会返回一个 promise。可以放心地在 loadCached 后面使用 .then。这就是 (*) 行中 Promise.resolve 的目的
Promisification#
- “Promisification” 指将一个接受回调的函数转换为一个返回 promise 的函数
- 由于许多函数和库都是基于回调的,所以将基于回调的函数和库 promisify 是有意义的
示例:
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);
}
// 用法:
// loadScript('path/script.js', (err, script) => {...})
// ...
// promisify
let loadScriptPromise = function (src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err);
else resolve(script);
});
});
};
// 用法:
// loadScriptPromise('path/script.js').then(...)
新的函数是对原始的 loadScript 函数的包装,在实际开发中,可能需要 promisify 很多函数
🚩promisify
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(...);
🚩promisification 函数的模块(module)
- https://github.com/digitaldesignlabs/es6-promisify
- 在 Node.js 中,有一个内建的 promisify 函数 util.promisify
🚩Promisification 场景
- Promisification 不是回调的完全替代
- 请记住,一个 promise 可能只有一个结果,但从技术上讲,一个回调可能被调用很多次
- 因此,promisification 仅适用于调用一次回调的函数。进一步的调用将被忽略
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构