JavaScript 使用 Promise
本文最后更新于:2021年6月21日 晚上
场景
为什么要使用 Promise?
JavaScript 异步发展史:回调函数 -> Promise -> async/await
传统异步使用回调函数,回调意味着嵌套,当你需要使用很多异步函数时,那你需要非常多的回调函数,可能形成回调地狱。
有问题就有人解决,js 没有多线程,所以天生就是异步的。正是因为异步的广泛性,所以很早之前就有人着力于解决异步回调的问题,github 上有很多已经废弃的库就是用于解决这个问题的。
然而现在,es6 出现了 Promise
,它能把嵌套回调压平为一层的链式调用,并且写进了 js 标准里。es7 甚至出现了更加优雅的方式,async/await
,能以同步的方式写异步的代码。当然,本质上只是 Promise 的一个语法糖,但其重要性也是不言而喻的——异步回调地狱已经不存在了!
说了这么多,那么平常我们应该怎么使用 Promise 呢?
使用 Promise
一般而言,我们作为使用者是无需创建 Promise 的,支持 Promise 的函数会返回一个 Promise 对象给我们,然后我们使用它的方法 then/catch
即可。
then()
:当前的 JavaScript 已经完成,要进行下一步的同步/异步操作了catch()
:用于捕获 Promise 链式调用中可能出现的错误
注:
then/catch
均返回一个新的 Promise
例如我们有这样一个需求
- 等待资源 A 加载完成
- 在 A 资源加载完成之后等待 B 资源加载完成
之前使用回调函数,我们的代码可能是这样的
/**
* 等待指定的时间/等待指定表达式成立
* @param {Number|Function} param 等待时间/等待条件
* @param {Function} callback 回调函数
*/
function wait(param, callback) {
if (typeof param === "number") {
setTimeout(callback, param);
} else if (typeof param === "function") {
var timer = setInterval(() => {
if (param()) {
clearInterval(timer);
callback();
}
}, 100);
} else {
callback();
}
}
wait(
() => document.querySelector("#a"),
() => {
wait(
() => document.querySelector("#b"),
() => {
console.log("a, b 两个资源已经全部加载完成");
}
);
}
);
// 结果
// a, b 两个资源已经全部加载完成
可以看到,上面如果还需要等待 c,d,e,f...
资源,那么回调函数的层级将是无法接受的。
现在,我们使用 Promise 改造一下代码
// 先不要管这个函数的具体实现,下面再说如何自己封装 Promise
/**
* 等待指定的时间/等待指定表达式成立
* @param {Number|Function} param 等待时间/等待条件
* @returns {Promise} Promise 对象
*/
function wait(param) {
return new Promise((resolve) => {
if (typeof param === "number") {
setTimeout(resolve, param);
} else if (typeof param === "function") {
var timer = setInterval(() => {
if (param()) {
clearInterval(timer);
resolve();
}
}, 100);
} else {
resolve();
}
});
}
wait(() => document.querySelector("#a"))
// 注意这里的 wait(() => document.querySelector('#b')) 同样是一个异步函数,返回了一个 Promise
// 接下来,有趣的地方来了
// 很明显,wait 是一个异步函数。wait 函数的 then 函数调用了另一个异步函数,然而 then 会等待异步执行完成,才继续执行后面的函数
.then(() => wait(() => document.querySelector("#b")))
// 这里仍然会等待上面的 Promise 完成之后才执行下面的内容
.then(() => console.log("a, b 两个资源已经全部加载完成"));
// 结果
// a, b 两个资源已经全部加载完成
下面我们尝试使用一下 catch
wait(() => document.querySelector("#a"))
.then(() => wait(() => document.querySelector("#b")))
.then(() => {
throw new Error("执行了某些操作发生了异常");
})
// 上面抛出了异常并且没有使用 catch 处理的话就会继续找下一个调用,直到找到处理的 catch,或者调用结束为止
.then(() => console.log("a, b 两个资源已经全部加载完成"))
// 捕获上面的 then() 发生的异常,保证后面的调用正常执行
.catch((error) => console.log("使用 catch 捕获的异常: ", error))
.then(() => console.log("测试异步函数结束"));
// 结果
// 使用 catch 捕获的异常: Error: 执行了某些操作发生了异常
// at wait.then.then (<anonymous>:4:11)
// VM272:9 测试异步函数结束
可以参考 MDN 上的教程 使用 Promises
封装 Promise
那么,你是否也对上面自定义的 wait
函数感到好奇呢?我们来详细的了解一下具体如何做到的吧!
/**
* 等待指定的时间/等待指定表达式成立
* @param {Number|Function} param 等待时间/等待条件
* @returns {Promise} Promise 对象
*/
function wait(param) {
// 这里返回了一个 Promise 对象,Promise 的构造函数要求一个函数参数
// 函数的参数实际上有两个,resolve 和 reject,分别代表 [已经完成] 和 [出现错误]
// 注:这个函数是立刻执行的,当 resolve 或 reject 执行时,这个 Promise 算是结束了,将进入下一个 then/catch 调用
return new Promise((resolve) => {
if (typeof param === "number") {
setTimeout(resolve, param);
} else if (typeof param === "function") {
var timer = setInterval(() => {
if (param()) {
clearInterval(timer);
// 这里执行了代码,如果有什么结果需要传递给下一个调用,则直接放到 resolve 函数内即可
resolve();
}
}, 100);
} else {
resolve();
}
});
}
同样的,我们也可以使用 Promise 封装其他函数
timeout
:一个简单的setTimeout()
的封装readLocal
:读取本地浏览器选择的文件timing
:测试函数执行的时间,不管是同步还是异步的(Promise)
/**
* 使用 Promise 简单封装 setTimeout
* @param {Number} ms 等待时间
* @returns {Promise} Promise 对象
*/
const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* 读取本地浏览器选择的文件
* @param {File} file 选择的文件
* @param {{String}} init 一些初始选项,目前只有 type 一项
* @returns {Promise} 返回了读取到的内容(异步)
*/
const readLocal = (() => {
const result = (file, { type = "readAsDataURL" } = {}) =>
new Promise((resolve, reject) => {
if (!file) {
reject("file not exists");
}
const fr = new FileReader();
fr.onload = (event) => {
resolve(event.target.result);
};
fr.onerror = (error) => {
reject(error);
};
fr[type](file);
});
result.DataURL = "readAsDataURL";
result.Text = "readAsText";
result.BinaryString = "readAsBinaryString";
result.ArrayBuffer = "readAsArrayBuffer";
return result;
})();
/**
* 测试函数的执行时间
* 注:如果函数返回 Promise,则该函数也会返回 Promise,否则直接返回执行时间
* @param {Function} fn 需要测试的函数
* @returns {Number|Promise} 执行的毫秒数
*/
function timing(fn) {
const begin = performance.now();
const result = fn();
if (!(result instanceof Promise)) {
return performance.now() - begin;
}
return result.then(() => performance.now() - begin);
}
吾辈建议你也可以封装一些常用的异步函数,下面会展示 JavaScript 中如何更简单的使用异步!
使用 async/await
async
:用于标识一个函数是异步函数,默认这个函数将返回一个 Promise 对象await
:用于在 async 函数内部使用的关键字,标识一个返回 Promise 的异步函数需要等待
使用 async/await
重构上面的代码
async function init() {
// await 等待异步函数执行完成
await wait(() => document.querySelector("#a"));
await wait(() => document.querySelector("#b"));
console.log("a, b 两个资源已经全部加载完成");
}
// 注:init() 函数将返回一个 Promise,我们可以继续追加下一步的操作
init();
是的,就是如此简单,直接在异步函数添加 await
关键字就好了!
最后,如果你要使用这些特性,请务必使用 babel 转换器。毕竟,有太多的人就是不肯升级浏览器。。。
可以参考