【重走JavaScript之高级程序设计】期约与异步函数
1.Promise期约
1.1 Promise基础
ES6新增引用类型Promise,通过new来实例化。
// 创建时必须需要传入执行器(executor)函数,这里传入给空函数以防止报错(抛出语法错误)。
let p = new Promise(() => {});
setTimeout(console.log,0 p) // Promise {<pending>}
1.1.1 Promise状态机
- pending(待定)
- fulfilled(兑现)
- rejected(拒绝)
pending为Promise的初始状态,Promise期约可以落定为兑现的fulfilled状态,或着失败的rejected状态。一旦落定,期约Promise就不能在更改状态。状态机是私有的,Promise故意将异步行为封装隔离开,JavaScript无法从外部直接修改。
1.1.2 解决值、拒绝理由及期约Promise用例
当状态机已经改变,说明期约已经完成。pending代表期约尚未开始或正在进行中,fulfilled代表期约已经兑现完成,而rejected代表期约已经拒绝完成。
期约Promise只要转变为兑现fulfilled,则会产生一个私有的内部值,解决值。期约Promise只要转变为拒绝rejected,则会产生一个私有的内部理由,拒绝理由。解决值和拒绝理由默认值都是undefined。
期约Promise的用例,假设向服务器发送一个请求并返回。如果返回状态码在200-299之间,期约状态变为兑现fulfilled,并且收到一个JSON字符串。如果返回状态码不在200-299之间,则期约状态变为拒绝rejected,拒绝理由可能是过Error对象,包含HTTP状态码及错误信息。
1.1.3 通过executor执行器函数控制期约Promise状态
期约状态是私有的,只能在内部进行操作。内部操作在执行器executor函数中完成。
executor执行器函数有两个作用
- 初始化期约Promise的异步行为
- 改变状态机,改变状态机主要是通过调用executor执行器函数的两个参数实现
- resolve(),把状态切换为兑现fulfilled
- reject(),把状态切换为拒绝rejected
executor执行器函数 是同步执行的
let p1 = new Promise(resolve => resolve());
setTimeout(console.log, 0, p1); // Promise {<resolved>: undefined}
let p2 = new Promise((resolve, reject) => reject());
setTimeout(console.log, 0, p2); // Promise {<rejected>: undefined} Uncaught (in promise) undefined
// 下面的例子可以解释上面的代码执行,
// executor执行器函数 是同步执行的,执行器函数是期约的初始化程序
new Promise(() => setTimeout(console.log, 0, "executor"));
setTimeout(console.log, 0, "Promise initialized");
// executor
// Promise initialized
由于添加setTimeout, 推迟切换状态
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));
setTimeout(console.log, 0, p); // Promise {<pending>}
无论resolve()和reject()哪个被调用,状态都不可撤销了,继续修改状态会静默失败。
let p = new Promise((resolve, reject) => {
resolve();
reject(); // 没有效果,静默失败
});
console.log(p); // Promise {<fulfilled>: undefined}
1.1.4 静态方法Promise.resolve()
Promise.resolve()方法返回一个期约Promise,传入的参数作为解决值。
以下两种方式完全一样
let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();
- 传入的参数作为解决值.
setTimeout(console.log, 0, Promise.resolve()); // Promise {<fulfilled>: undefined}
setTimeout(console.log, 0, Promise.resolve(3)); // Promise {<fulfilled>: 3}
- 如果传入的参数本身是个期约,那么它的行为就类似于一个空包装。并且具有幂等性。
let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(7))); // true
- 使用这个静态方法,可以把任何值都包装为一个期约Promise。
// 由于这个特性,使用resolve包装了错误,期约显示成功,却提示错误。
let p = Promise.resolve(new Error("foo"));
setTimeout(console.log, 0, p); // Promise {<fulfilled>: Error: foo
1.1.5 静态方法Promise.reject()
Promise.reject()方法返回一个期约Promise,传入的参数作为拒绝理由。这个参数也会传给后续的拒绝处理程序。
let p = Promise.reject(3);
setTimeout(console.log, 0, p); // Promise {<rejected>: 3}
p.then(null, e => setTimeout(console.log, 0, e)); // 3
Promise.reject() 并没有照搬Promise.resolve()的幂等逻辑。如果传给它一个期约对象,则这个期约会变成它返回的拒绝期约的理由。
setTimeout(console.log, 0, Promise.reject(Promise.resolve())); // Promise {<rejected>: Promise <resolved>}
1.2 期约的实例方法
1.2.1 Promise.prototype.then()
Promise.prototyoe.then()是为期约添加处理程序的方法。then方法有两个参数,提供的话会分别进入fulfilled状态和rejected状态的处理程序。
- 第一个参数 onResolved()处理程序,该处理程序的返回值会通过Promise.resolve()包装来生成新期约。
- 第二参数 onRejected()处理程序,如果只想提供onRejected,需要用undefined占位
let p1 = Promise.resolve("foo");
let p2 = p1.then(); // Promise {<fulfilled>: 'foo'}。 then不处理程序,原样向后传。
let p3 = p1.then(() => undefined); // Promise {<fulfilled>: undefined}
let p4 = p1.then(() => {}); // Promise {<fulfilled>: undefined}
let p5 = p1.then(() => Promise.resolve()); // Promise {<fulfilled>: undefined}
let p6 = p1.then(() => "bar"); // Promise {<fulfilled>: 'bar'}
let p7 = p1.then(() => Promise.resolve("bar")); // Promise {<fulfilled>: 'bar'}
let p8 = p1.then(() => new Promise(() => {})); // Promise {<pending>}
let p9 = p1.then(() => Promise.reject()); // Promise {<rejected>: undefined}
let p10 = p1.then(() => {
throw "baz";
}); //Promise {<rejected>: 'baz'}
let p11 = p1.then(() => Error("qux")); // Promise {<fulfilled>: Error: qux。把错误的对象包装在一个fulfilled的Promise期约中。
// onRejected处理程序的值也会被Promise.resolve()包装。这有点违法直觉,但是onRejected处理程序就是为了捕获错误,不抛出异常是符合期约的行为,应该返回一个解决的期约。
let p1 = Promise.reject("foo");
let p2 = p1.then(); // Promise {<rejected>: 'foo'}。 then不处理程序,原样向后传。
let p3 = p1.then(null, () => undefined); // Promise {<fulfilled>: undefined}
let p4 = p1.then(null, () => {}); // Promise {<fulfilled>: undefined}
let p5 = p1.then(null, () => Promise.resolve()); // Promise {<fulfilled>: undefined}
let p6 = p1.then(null, () => "bar"); // Promise {<fulfilled>: 'bar'}
let p7 = p1.then(null, () => Promise.resolve("bar")); // Promise {<fulfilled>: 'bar'}
let p8 = p1.then(null, () => new Promise(() => {})); // Promise {<pending>}
let p9 = p1.then(null, () => Promise.reject()); // Promise {<rejected>: undefined}
let p10 = p1.then(null, () => {
throw "baz";
}); //Promise {<rejected>: 'baz'}
let p11 = p1.then(null, () => Error("qux")); // Promise {<fulfilled>: Error: qux
1.2.2 Promise.prototyoe.catch()
Promise.prototyoe.catch()是为期约添加拒绝处理程序。这个方法只接收一个参数,onnRejected()处理程序。本质上这个方法就是给语法糖,相当于调用Promise.prototype.then(null, onRejected)。
let p = Promise.reject();
let onRejected = e => setTimeout(console.log, 0, "onRejected");
p.then(null, onRejected); // onRejected
let p1 = p.catch(onRejected); // onRejected
Promise.prototyoe.catch() 返回一个新期约,行为与调用then方法的onRejected处理程序相同。
let p1 = new Promise(() => {}); // Promise {<pending>}
let p2 = p1.catch(); // Promise {<pending>}
console.log(p1 === p2); // false
1.2.3 Promise.prototyoe.finally()
Promise.prototyoe.finally()是用于给期约添加onFinally处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。这个方法可以避免onResolved和onRejected处理程序出现冗余代码,但onFinally这个方法没法知道期约的状态是解决还是拒绝。
onFinally被设定为一个状态无关的方法,大多数情况下表现为父期约的传递,对于fulfilled和rejected状态都是如此。
let p1 = Promise.resolve("foo"); // Promise {<fulfilled>: 'foo'}
let p2 = p1.finally(); // Promise {<fulfilled>: 'foo'}
let p3 = p1.finally(() => undefined); // Promise {<fulfilled>: 'foo'}
let p4 = p1.finally(() => {}); // Promise {<fulfilled>: 'foo'}
let p5 = p1.finally(() => Promise.resolve()); // Promise {<fulfilled>: 'foo'}
let p6 = p1.finally(() => "bar"); // Promise {<fulfilled>: 'foo'}
let p7 = p1.finally(() => Promise.resolve("bar")); // Promise {<fulfilled>: 'foo'}
let p8 = p1.finally(() => Error("qux")); // Promise {<fulfilled>: 'foo'}
let p9 = p1.finally(() => new Promise(() => {})); // Promise {<pending>}
let p10 = p1.finally(() => Promise.reject()); // Promise {<rejected>: undefined}
let p11 = p1.finally(() => {
throw "baz"; // Promise {<rejected>: 'baz'}
});
1.2.4 非重入期约(微任务)
当期约Promise进入落定,与该状态相关的处理程序仅仅会被排期,而非立即执行。在处理程序的同步代码会在处理程序之前先执行。
// 处理程序是微任务
let p1 = Promise.resolve();
p1.then(() => console.log("p1.then() onResolved"));
console.log("p1.then() returns");
let p2 = Promise.reject();
p2.then(null, () => console.log("p2.then() onRejected"));
console.log("p2.then() returns");
let p3 = Promise.reject();
p3.catch(() => console.log("p3.catch() onRejected"));
console.log("p3.catch() returns");
let p4 = Promise.resolve();
p4.finally(() => console.log("p4.finally() onFinally"));
console.log("p4.finally() returns");
// p1.then() returns
// p2.then() returns
// p3.catch() returns
// p4.finally() returns
// p1.then() onResolved
// p2.then() onRejected
// p3.catch() onRejected
// p4.finally() onFinally
1.2.5 临近处理程序的执行顺序(微任务顺序)
如果给期约添加多个处理程序,那么这些处理程序会按照添加的顺序执行。
let p1 = Promise.resolve();
p1.then(() => console.log("p1.then() onResolved"));
p1.then(() => console.log("p1.then() onResolved2"));
p1.then(() => console.log("p1.then() onResolved3"));
1.2.6 拒绝期约与拒绝错误的处理
在期约Promise中抛出错误是从消息队列中异步抛出的,所以并不会阻止运行时继续执行同步指令。
throw Error("foo");
console.log("bar"); // 这一行不会执行
// 这可能是一种副作用
Promise.reject(Error("foo"));
console.log("bar"); // 这一行会执行
异步的错误只能通过异步的onRejected处理程序捕获。
// 正确
Promise.reject(Errpo("foo")).catch(e => console.log(e.message));
// 不正确
try {
Promise.reject(Error("foo"));
} catch (e) {
console.log(e.message);
}
then()和catch()的onRejected处理程序在语义上相当于try/catch。出发点都是捕获错误之后将其隔离,同时不影响正常逻辑执行。
为此,onRejected处理程序的任务应该是在捕获异步错误之后返回一个解决的期约。
下面展示了同步和异步处理错误的区别
// 同步处理错误
console.log("begin synchronous execution");
try {
throw Error("foo");
} catch (e) {
console.log("caught error", e);
}
console.log("continue synchronous execution");
// begin synchronous execution
// caught error Error: foo
// continue synchronous execution
// 异步处理错误
new Promise((resolve,reject)=>{
console.log('begin asynchronous execution')
reject(Error('bar))
}).catch(e=>{
console.log('caught error',e)
}).then(()=>{
console.log('continue asynchronous execution')
})
// begin asynchronous execution
// caught error Error: bar
// continue asynchronous execution
1.3 期约连锁和期约合成
1.3.1 期约连锁
让每个执行器返回一个期约实例。这样就可以让每个后续期约都等待之前的期约,也就是串行化异步任务。
let p1 = new Promise((resolve, reject) => {
console.log("p1 executor");
setTimeout(resolve, 1000);
});
p1.then(
() =>
new Promise((resolve, reject) => {
console.log("p2 executor");
setTimeout(resolve, 1000);
})
)
.then(
() =>
new Promise((resolve, reject) => {
console.log("p3 executor");
setTimeout(resolve, 1000);
})
)
.then(
() =>
new Promise((resolve, reject) => {
console.log("p4 executor");
setTimeout(resolve, 1000);
})
);
// p1 executor 1秒后执行
// p2 executor 2秒后执行
// p3 executor 3秒后执行
// p4 executor 4秒后执行
上面的例子可以使用工厂函数完成,每一个后续的期约都会等待前一个期约解决。然后实例化一个新期约并返回它。简化异步任务串行化。
function delayedResolve(str) {
return new Promise((resolve, reject) => {
console.log(str);
setTimeout(resolve, 1000);
});
}
delayedResolve("p1 executor")
.then(() => delayedResolve("p2 executor"))
.then(() => delayedResolve("p3 executor"))
.then(() => delayedResolve("p4 executor"));
// p1 executor 1秒后执行
// p2 executor 2秒后执行
// p3 executor 3秒后执行
// p4 executor 4秒后执行
then()、catch() 和 finally() 都返回期约,串联这些方法也很客观
let p = new Promise((resolve, reject) => {
console.log("initial promise rejects");
reject();
});
p.catch(() => console.log("reject handler"))
.then(() => console.log("resolve handler"))
.finally(() => console.log("finally handler"));
// initial promise rejects
// reject handler
// resolve handler
// finally handler
1.3.2 期约图
因为一个期约可以有任意多个处理程序,所以期约连锁可以构建有向非循环图的结构。这样,每个期约都是图中一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中每个节点都会等待前一个节点的落定,所以图的方向就是期约的解决或拒绝顺序。
A
/ \
B C
/ \ / \
D E F G
日志的输出语句是对二叉树的层序遍历。期约Promise的处理程序是按照它们添加的顺序执行的。由于期约的处理程序是先添加到消息队列,然后才逐个执行,因此构成了层序遍历。
let A = new Promise((resolve, reject) => {
console.log("A");
resolve();
});
let B = A.then(() => console.log("B"));
let C = A.then(() => console.log("C"));
B.then(() => console.log("D"));
B.then(() => console.log("E"));
C.then(() => console.log("F"));
C.then(() => console.log("G"));
// A
// B
// D
// E
// C
// F
// G
树只是期约图的一种形式。考虑到根节点不一定唯一,且多个期约也可以组合成一个期约(Promise.all()和Promise.race()),所以有向非循环图上体现期约连锁可能性的最准确表达。
1.3.3 期约合成
Promise类提供两个将多个期约实例组合成一个期约的静态方法: Promise.all()和Promise.race()。而合成后的期约行为取决于内部期约行为。
- Promise.all()
Promise.all() 接受一组可迭代的对象,静态方法创建的期约会在所有期约解决后再解决,反之有一个拒绝了所有期约都会拒绝。
// 合成的期约只会在所有期约都解决之后才解决。
let p = Promise.all([Promise.resolve(), new Promise((resolve, reject) => setTimeout(resolve, 1000))]);
setTimeout(console.log, 0, p); // Promise {<pending>}
p.then(() => setTimeout(console.log, 0, "all resolved")); // all resolved 一秒后
// 如果有一个期约待定,则合成的期约也会待定。
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise {<pending>}
// 有一个期约被拒绝了,则合成的期约也被拒绝
let p2 = Promise.all([Promise.resolve(), Promise.reject(), Promise.resolve()]);
setTimeout(console.log, 0, p2); // Promise {<rejected>: undefined}
// 如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器的顺序。
let p3 = Promise.all([Promise.resolve(1), Promise.resolve(), Promise.resolve(3)]);
p3.then(values => setTimeout(console.log, 0, values)); // [1, undefined, 3]
// 如果遇到期约拒绝了,则会将第一个期约的拒绝原因作为合成期约的拒绝原因,后续错误会被静默忽略。
let p4 = Promise.all([Promise.reject(3), new Promise((resolve, reject) => setTimeout(reject, 1000))]);
p4.catch(reason => setTimeout(console.log, 0, reason)); // 3
- Promise.race()
Promise.race() 接受一组可迭代的对象,静态方法创建的期约会在第一个期约解决或拒绝后解决或拒绝。
Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约。
// 解决先发生的,超时后的拒绝被忽略
let p1 = Promise.race([Promise.resolve(3), new Promise((resolve, reject) => setTimeout(reject, 1000))]);
setTimeout(console.log, 0, p1); // Promise {<fulfilled>: 3}
// 拒绝先发生的,超时后的解决被忽略
let p2 = Promise.race([Promise.reject(4), new Promise((resolve, reject) => setTimeout(resolve, 1000))]);
setTimeout(console.log, 0, p2); // Promise {<rejected>: 4}
// 迭代顺序决定了落定顺序
let p3 = Promise.race([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]);
setTimeout(console.log, 0, p3); // Promise {<fulfilled>: 1}
// 如果遇到期约拒绝了,则会将第一个拒绝的原因作为合成期约的拒绝原因,后续错误会被静默忽略。
let p4 = Promise.race([Promise.reject(3), new Promise((resolve, reject) => setTimeout(reject, 1000))]);
p4.catch(reason => setTimeout(console.log, 0, reason)); // 3
1.3.4 串行期约的合成
异步产生值并将其传给处理程序,基于后续期约使用之前期约的返回值来串行期约。
function addTwo(x) {
return x + 2;
}
function addThree(x) {
return x + 3;
}
function addFive(x) {
return x + 5;
}
function addTen(x) {
return [addTwo, addThree, addFive].reduce((promise, fn) => promise.then(fn), Promise.resolve(x));
}
addTen(5).then(console.log); // 15
// 也可以这样写
function clain(...fns) {
return x => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x));
}
let addTen = clain(addTwo, addThree, addFive);
addTen(5).then(console.log); // 15
1.4 期约扩展
1.4.1 期约的取消
class CancelToken {
constructor() {
this.promise = new Promise((resolve, reject) => {
cancalFn(() => {
setTimeout(console.log, 0, "delay cancelled");
resolve();
});
});
}
}
const startButton = document.getElementById("start");
const cancalButton = document.getElementById("cancal");
function cancellabelDelayedResolve(delay) {
setTimeout(console.log, 0, "set delay");
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
setTimeout(console.log, 0, "delay cancelled");
resolve();
}, delay);
const cancelToken = new cancelToken(cancelCallback => {
cancalButton.addEventListener("click", cancelCallback);
});
cancelToken.promise.then(() => {
clearTimeout(id);
});
});
}
1.4.2 期约进度通知
监控期约的执行进度。
class TrackablePromise extends Promise {
constructor(executor) {
const notifyHandlers = [];
super((resolve, reject) => {
return executor(resolve, reject, status => {
notifyHandlers.map(handler => handler(status));
});
});
this.notifyHandlers = notifyHandlers;
}
notify(notifyHandler) {
this.notifyHandlers.push(notifyHandler);
return this;
}
}
let p = new TrackablePromise((resolve, reject, notify) => {
function countdown(x) {
if (x > 0) {
notify(`${20 * x}% remaining`);
setTimeout(() => countdown(x - 1), 1000);
} else {
resolve();
}
}
countdown(10);
});
p.notify(x => setTimeout(console.log, 0, "progress:", x));
p.then(() => setTimeout(console.log, 0, "done"));
// TrackablePromise {<pending>, notifyHandlers: Array(0)}
// progress: 80% remaining
// progress: 60% remaining
// progress: 40% remaining
// progress: 20% remaining
// done
// notify 返回期约,可以连续调用
p.notify(x => setTimeout(console.log, 0, "a:", x)).notify(x => setTimeout(console.log, 0, "b:", x));
p.then(() => setTimeout(console.log, 0, "done"));
// a: 80% remaining
// b: 80% remaining
// a: 60% remaining
// b: 60% remaining
// a: 40% remaining
// b: 40% remaining
// a: 20% remaining
// b: 20% remaining
// done
2. 异步函数
异步函数,也称为 async/await,是ES6期约模式在函数中的应用,async/await 是ES8规范新增的。
2.1 异步函数
2.1.1 async
async 关键字用于声明异步函数。
// 函数声明
async function foo() {}
// 函数表达式
let bar = async function () {};
// 箭头函数
let baz = async () => {};
// 类方法
class Qux {
async qux() {}
}
异步函数始终返回期约对象Promise。
异步函数如果使用return返回了值,这个值会被Promise.resolve()包装,如果没有return,则返回Promise.resolve(undefined)。
async function foo() {
console.log(1);
return 3; // 等同 return Promise.resolve(3);
}
foo().then(console.log); // Promise {<fulfilled>: 1}
console.log(2);
// 1
// 2
// 3
异步函数如果直接返回值则会被当作已解决的期约,返回带thenable的Promise对象给后续处理程序解包,。
async function qux() {
return Promise.resolve("qux");
}
qux().then(console.log); // qux
async function foo() {
console.log(1);
Promise.reject(3);
}
foo().catch(console.log);
console.log(2);
// 1
// 2
// Uncaught (in promise) 3
2.1.2 await
await 等待期约的解决。await关键字可以暂停执行异步函数代码,让出JavaScipt运行时的执行线程(异步改同步)
单独的Promise.reject()不会被异步函数捕获,而会抛出未捕获错误。不过,对拒绝的期约使用await则会释放unwrap错误值(将拒绝期约返回)
async function qux() {
console.log(await Promise.resolve(qux));
}
qux(); //qux
async function foo() {
console.log(1);
await Promise.reject(3);
}
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3
2.1.3 await的限制
-
await 关键字只能在异步函数中使用,不能在普通函数中使用。
-
await 关键字必须在异步函数内部,不能在函数外部。注意嵌套函数。
// 不允许await出现在箭头函数中
function foo(){
const syncFn= () =>{
return await Promise.resolve('foo')
}
console.log(syncFn())
}
// 不允许await出现在同步函数中
function bar(){
function syncFn(){
return await Promise.resolve('bar')
}
console.log(syncFn())
}
// 不允许await出现在同步函数表达式中
function baz(){
const syncFn= () =>{
return await Promise.resolve('baz')
}
console.log(syncFn())
}
2.2 停止和恢复执行
async/await 中真正起作用的是await。await关键字不过是一个标识符(修饰符)。异步函数如果不包含await关键字,其执行和普通函数没有区别。
async function foo() {
console.log(1);
}
console.log(1);
foo();
console.log(3);
// 1
// 1
// 3
要完全理解await关键字,必须知道它并非只是等待一个值那么简单。JavaScript运行时在碰到await关键字时,会记录在哪里暂停执行。等到await右边的值可用了,JavaScript运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
实际开发中,并于并行的异步操作我们通常更关注结果,而不依赖顺序执行,但是面试要问。
async function foo() {
console.log(2);
console.log(await Promise.resolve(8)); // 暂停执行,将代码及后续代码推入微任务队列
console.log(9);
}
async function bar() {
console.log(4);
console.log(await 6); // 暂停执行,将代码及后续代码推入微任务队列
console.log(7);
}
console.log(1);
foo();
console.log(3);
bar();
console.log(5);
// 依次打印 1 2 3 4 5 8 9 6 7 理解微任务的暂停与执行
// 同步执行并打印 1 2 3 4,8和6被推入微任务队列,待同步执行完后,依次输出8 9 6 7
2.3 异步函数的策略
2.3.1 实现slepp()
function sleep(delay) {
return new Promise(resolve => setTimeout(resolve, delay));
}
async function foo() {
const to = Date.now();
await sleep(1000);
console.log(Date.now() - to);
}
foo();
2.3.2 利用平行执行
下面这些期约。异步函数会依次暂停,等待每个超时完成,这样可以保证执行顺序,但总执行时间会变长。
async function randomDelay(id) {
const delay = Math.random() * 1000;
return new Promise(resolve =>
setTimeout(() => {
console.log(`${id} finished`);
resolve();
}, delay)
);
}
async function foo(){
const t0 = Date.now();
for(let i=0,i<5,i++){
await randomDelay(i);
}
console.log(`${Date.now() - to}ms slapsed`)
}
但是上面的期约之间没有依赖,不需要按照顺序执行呢。
async function randomDelay(id) {
const delay = Math.random() * 1000;
return new Promise(resolve =>
setTimeout(() => {
console.log(`${id} finished`);
resolve();
}, delay)
);
}
async function foo() {
const t0 = Date.now();
const promises = Array(5)
.fill(null)
.map((_, i) => randomDelay(i));
for (const p of promises) {
await p;
}
console.log(`${Date.now() - t0}ms slapsed`);
}
2.3.3 串行执行期约
使用async/await 期约连锁会变得很简单。
async function addTwo(x) {
return x + 2;
}
async function addThree(x) {
return x + 3;
}
async function addFive(x) {
return x + 5;
}
async function addTen(x) {
for (const fn of [addTwo, addThree, addFive]) {
x = await fn(x);
}
return x;
}
addTen(9).then(console.log); // 19
2.3.4 栈追踪与内存管理
期约和异步函数功能有相当部分的重叠,但他们在内存中的差别很大。
栈追踪信息应该相当直接地表现JavaScript引擎当前栈内存中函数调用之间的嵌套关系。
在超时处理程序执行时和拒绝期约时,我们看到的错误信息包含嵌套函数的标识符,那是被调用以创建最初期约实例的函数。可是我们知道这些函数已经返回了,因此栈追踪信息中不应该看到他们。
JavaScript引擎会在创建期约时尽可能保留完整的调用栈。在抛出错误时,调用栈可以由运行时的错误处理逻辑获取,因而就会出现在栈追踪信息中。当然,这意味着栈追踪信息会占用内存,从来带来一些计算和存储的成本。
// 如果该用异步函数
function fooPromiseExecutor(resolve, reject) {
setTimeout(reject, 1000, "bar");
}
function foo() {
new Promise(fooPromiseExecutor);
}
foo();
// Uncaught (in promise) bar
// setTimeout (async)
// fooPromiseExecutor @ VM80:2
// foo @ VM80:6
// (anonymous) @ VM80:9
这样写,栈追踪信息就准确地反应当前的调用栈,fooPromiseExecutor已经返回,所以它不在错误信息中。但foo()此时被挂起,并没有退出。Javascript运行时可以简单地在嵌套函数中存储指向包含函数的指针,就跟对待同步函数调用栈一样。这个指针式机存储在内存中,可用于在出错时生成栈追踪信息。这样就不会像之前的例子那样带来额外的消耗,因此在重视性能的应用中是可以优先考虑的。
function fooPromiseExecutor(resolve, reject) {
setTimeout(reject, 1000, "bar");
}
async function foo() {
await new Promise(fooPromiseExecutor);
}
foo();
// foo
// async function (async)
// foo
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现