北太平庄网站建设,个人租用境外服务器,网站关键词优化方案分为几个步骤,wordpress v5.0本文参考自电子书《ECMAScript 6 入门》#xff1a;https://es6.ruanyifeng.com/ Promise 对象
1. Promise 的含义
Promise 是异步编程的一种解决方案#xff0c;比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现#xff0c;ES6 将其写进了… 本文参考自电子书《ECMAScript 6 入门》https://es6.ruanyifeng.com/ Promise 对象
1. Promise 的含义
Promise 是异步编程的一种解决方案比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现ES6 将其写进了语言标准统一了用法原生提供了 Promise 对象。
所谓 Promise简单说就是一个容器里面保存着某个未来才会结束的事件通常是一个异步操作的结果。从语法上说Promise 是一个对象从它可以获取异步操作的消息。Promise 提供统一的 API各种异步操作都可以用同样的方法进行处理。
Promise 对象有以下两个特点。 对象的状态不受外界影响。Promise 对象代表一个异步操作有三种状态pending进行中、fulfilled已成功和rejected已失败。只有异步操作的结果可以决定当前是哪一种状态任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来它的英语意思就是“承诺”表示其他手段无法改变。 一旦状态改变就不会再变任何时候都可以得到这个结果。Promise 对象的状态改变只有两种可能从 pending 变为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生状态就凝固了不会再变了会一直保持这个结果这时就称为 resolved已定型。如果改变已经发生了你再对 Promise 对象添加回调函数也会立即得到这个结果。这与事件Event完全不同事件的特点是如果你错过了它再去监听是得不到结果的。
注意为了行文方便本章后面的 resolved 统一只指 fulfilled 状态不包含 rejected 状态。
有了 Promise 对象就可以将异步操作以同步操作的流程表达出来避免了层层嵌套的回调函数。此外Promise 对象提供统一的接口使得控制异步操作更加容易。
Promise 也有一些缺点。首先无法取消 Promise一旦新建它就会立即执行无法中途取消。其次如果不设置回调函数Promise 内部抛出的错误不会反应到外部。第三当处于 pending 状态时无法得知目前进展到哪一个阶段刚刚开始还是即将完成。
如果某些事件不断地反复发生一般来说使用 Stream 模式是比部署 Promise 更好的选择。
2. 基本用法
ES6 规定Promise对象是一个构造函数用来生成Promise实例。
下面代码创造了一个Promise实例。
const promise new Promise(function(resolve, reject) {// ... some codeif (/* 异步操作成功 */){resolve(value);} else {reject(error);}
});Promise 构造函数接受一个函数作为参数该函数的两个参数分别是 resolve 和 reject。它们是两个函数由 JavaScript 引擎提供不用自己部署。
resolve 函数的作用是将 Promise 对象的状态从“未完成”变为“成功”即从 pending 变为 resolved在异步操作成功时调用并将异步操作的结果作为参数传递出去reject 函数的作用是将 Promise 对象的状态从“未完成”变为“失败”即从 pending 变为 rejected在异步操作失败时调用并将异步操作报出的错误作为参数传递出去。
Promise 实例生成以后可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。
promise.then(function(value) {// success
}, function(error) {// failure
});then 方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为 resolved 时调用第二个回调函数是 Promise 对象的状态变为 rejected 时调用。这两个函数都是可选的不一定要提供。它们都接受 Promise 对象传出的值作为参数。
下面是一个 Promise 对象的简单例子。
function timeout(ms) {return new Promise((resolve, reject) {setTimeout(resolve, ms, done);});
}timeout(100).then((value) {console.log(value);
});上面代码中timeout 方法返回一个 Promise 实例表示一段时间以后才会发生的结果。过了指定的时间ms 参数以后Promise 实例的状态变为 resolved就会触发 then 方法绑定的回调函数。
Promise 新建后就会立即执行。
let promise new Promise(function(resolve, reject) {console.log(Promise);resolve();
});promise.then(function() {console.log(resolved.);
});console.log(Hi!);// Promise
// Hi!
// resolved上面代码中Promise 新建后立即执行所以首先输出的是 Promise。然后then 方法指定的回调函数将在当前脚本所有同步任务执行完才会执行所以 resolved 最后输出。
下面是异步加载图片的例子。
function loadImageAsync(url) {return new Promise(function(resolve, reject) {const image new Image();image.onload function() {resolve(image);};image.onerror function() {reject(new Error(Could not load image at url));};image.src url;});
}上面代码中使用Promise包装了一个图片加载的异步操作。如果加载成功就调用resolve方法否则就调用reject方法。
下面是一个用Promise对象实现的 Ajax 操作的例子。
const getJSON function(url) {const promise new Promise(function(resolve, reject){const handler function() {if (this.readyState ! 4) {return;}if (this.status 200) {resolve(this.response);} else {reject(new Error(this.statusText));}};const client new XMLHttpRequest();client.open(GET, url);client.onreadystatechange handler;client.responseType json;client.setRequestHeader(Accept, application/json);client.send();});return promise;
};getJSON(/posts.json).then(function(json) {console.log(Contents: json);
}, function(error) {console.error(出错了, error);
});上面代码中getJSON 是对 XMLHttpRequest 对象的封装用于发出一个针对 JSON 数据的 HTTP 请求并且返回一个 Promise 对象。需要注意的是在 getJSON 内部resolve 函数和 reject 函数调用时都带有参数。
如果调用 resolve 函数和 reject 函数时带有参数那么它们的参数会被传递给回调函数。reject 函数的参数通常是 Error 对象的实例表示抛出的错误resolve 函数的参数除了正常的值以外还可能是另一个 Promise 实例比如像下面这样。
const p1 new Promise(function (resolve, reject) {// ...
});const p2 new Promise(function (resolve, reject) {// ...resolve(p1);
})上面代码中p1 和 p2 都是 Promise 的实例但是 p2 的 resolve 方法将 p1 作为参数即一个异步操作的结果是返回另一个异步操作。
注意这时 p1 的状态就会传递给 p2也就是说p1 的状态决定了 p2 的状态。如果 p1 的状态是 pending那么 p2 的回调函数就会等待 p1 的状态改变如果 p1 的状态已经是 resolved 或者 rejected那么 p2 的回调函数将会立刻执行。
const p1 new Promise(function (resolve, reject) {setTimeout(() reject(new Error(fail)), 3000)
})const p2 new Promise(function (resolve, reject) {setTimeout(() resolve(p1), 1000)
})p2.then(result console.log(result)).catch(error console.log(error))
// Error: fail上面代码中p1 是一个 Promise3 秒之后变为 rejected。p2 的状态在 1 秒之后改变resolve 方法返回的是 p1。由于 p2 返回的是另一个 Promise导致 p2 自己的状态无效了由 p1 的状态决定 p2 的状态。所以后面的 then 语句都变成针对后者p1。又过了 2 秒p1 变为 rejected导致触发 catch 方法指定的回调函数。
注意调用 resolve 或 reject 并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) {resolve(1);console.log(2);
}).then(r {console.log(r);
});
// 2
// 1上面代码中调用 resolve(1) 以后后面的 console.log(2) 还是会执行并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行总是晚于本轮循环的同步任务。
一般来说调用 resolve 或 reject 以后Promise 的使命就完成了后继操作应该放到 then 方法里面而不应该直接写在 resolve 或 reject 的后面。所以最好在它们前面加上 return 语句这样就不会有意外。
new Promise((resolve, reject) {return resolve(1);// 后面的语句不会执行console.log(2);
})3. Promise.prototype.then()
Promise 实例具有 then 方法也就是说then 方法是定义在原型对象 Promise.prototype 上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过then 方法的第一个参数是 resolved 状态的回调函数第二个参数是 rejected 状态的回调函数它们都是可选的。
then 方法返回的是一个新的 Promise 实例注意不是原来那个 Promise 实例。因此可以采用链式写法即 then 方法后面再调用另一个 then 方法。
getJSON(/posts.json).then(function(json) {return json.post;
}).then(function(post) {// ...
});上面的代码使用 then 方法依次指定了两个回调函数。第一个回调函数完成以后会将返回结果作为参数传入第二个回调函数。
采用链式的 then可以指定一组按照次序调用的回调函数。这时前一个回调函数有可能返回的还是一个 Promise 对象即有异步操作这时后一个回调函数就会等待该 Promise 对象的状态发生变化才会被调用。
getJSON(/post/1.json).then(function(post) {return getJSON(post.commentURL);
}).then(function (comments) {console.log(resolved: , comments);
}, function (err){console.log(rejected: , err);
});上面代码中第一个 then 方法指定的回调函数返回的是另一个 Promise 对象。这时第二个 then 方法指定的回调函数就会等待这个新的 Promise 对象状态发生变化。如果变为 resolved就调用第一个回调函数如果状态变为 rejected就调用第二个回调函数。
如果采用箭头函数上面的代码可以写得更简洁。
getJSON(/post/1.json).then(post getJSON(post.commentURL)
).then(comments console.log(resolved: , comments),err console.log(rejected: , err)
);4. Promise.prototype.catch()
Promise.prototype.catch() 方法是 .then(null, rejection) 或 .then(undefined, rejection) 的别名用于指定发生错误时的回调函数。
getJSON(/posts.json).then(function(posts) {// ...
}).catch(function(error) {// 处理 getJSON 和 前一个回调函数运行时发生的错误console.log(发生错误, error);
});上面代码中getJSON() 方法返回一个 Promise 对象如果该对象状态变为 resolved则会调用 then() 方法指定的回调函数如果异步操作抛出错误状态就会变为 rejected就会调用 catch() 方法指定的回调函数处理这个错误。另外then() 方法指定的回调函数如果运行中抛出错误也会被 catch() 方法捕获。
p.then((val) console.log(fulfilled:, val)).catch((err) console.log(rejected, err));// 等同于
p.then((val) console.log(fulfilled:, val)).then(null, (err) console.log(rejected:, err));下面是一个例子。
const promise new Promise(function(resolve, reject) {throw new Error(test);
});
promise.catch(function(error) {console.log(error);
});
// Error: test上面代码中promise抛出一个错误就被catch()方法指定的回调函数捕获。注意上面的写法与下面两种写法是等价的。
// 写法一
const promise new Promise(function(resolve, reject) {try {throw new Error(test);} catch(e) {reject(e);}
});
promise.catch(function(error) {console.log(error);
});// 写法二
const promise new Promise(function(resolve, reject) {reject(new Error(test));
});
promise.catch(function(error) {console.log(error);
});比较上面两种写法可以发现reject()方法的作用等同于抛出错误。
如果 Promise 状态已经变成resolved再抛出错误是无效的。
const promise new Promise(function(resolve, reject) {resolve(ok);throw new Error(test);
});
promise.then(function(value) { console.log(value) }).catch(function(error) { console.log(error) });
// ok上面代码中Promise 在resolve语句后面再抛出错误不会被捕获等于没有抛出。因为 Promise 的状态一旦改变就永久保持该状态不会再变了。
Promise 对象的错误具有“冒泡”性质会一直向后传递直到被捕获为止。也就是说错误总是会被下一个catch语句捕获。
getJSON(/post/1.json).then(function(post) {return getJSON(post.commentURL);
}).then(function(comments) {// some code
}).catch(function(error) {// 处理前面三个Promise产生的错误
});上面代码中一共有三个 Promise 对象一个由 getJSON() 产生两个由 then() 产生。它们之中任何一个抛出的错误都会被最后一个 catch() 捕获。
一般来说不要在 then() 方法里面定义 Reject 状态的回调函数即 then 的第二个参数总是使用 catch 方法。
// bad
promise.then(function(data) {// success}, function(err) {// error});// good
promise.then(function(data) { //cb// success}).catch(function(err) {// error});上面代码中第二种写法要好于第一种写法理由是第二种写法可以捕获前面 then 方法执行中的错误也更接近同步的写法try/catch。因此建议总是使用 catch() 方法而不使用 then() 方法的第二个参数。
跟传统的 try/catch 代码块不同的是如果没有使用 catch() 方法指定错误处理的回调函数Promise 对象抛出的错误不会传递到外层代码即不会有任何反应。
const someAsyncThing function() {return new Promise(function(resolve, reject) {// 下面一行会报错因为x没有声明resolve(x 2);});
};someAsyncThing().then(function() {console.log(everything is great);
});setTimeout(() { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123上面代码中someAsyncThing() 函数产生的 Promise 对象内部有语法错误。浏览器运行到这一行会打印出错误提示 ReferenceError: x is not defined但是不会退出进程、终止脚本执行2 秒之后还是会输出 123。这就是说Promise 内部的错误不会影响到 Promise 外部的代码通俗的说法就是“Promise 会吃掉错误”。
这个脚本放在服务器执行退出码就是 0即表示执行成功。不过Node.js 有一个 unhandledRejection 事件专门监听未捕获的 reject 错误上面的脚本会触发这个事件的监听函数可以在监听函数里面抛出错误。
process.on(unhandledRejection, function (err, p) {throw err;
});上面代码中unhandledRejection 事件的监听函数有两个参数第一个是错误对象第二个是报错的 Promise 实例它可以用来了解发生错误的环境信息。
注意Node 有计划在未来废除 unhandledRejection 事件。如果 Promise 内部有未捕获的错误会直接终止进程并且进程的退出码不为 0。
再看下面的例子。
const promise new Promise(function (resolve, reject) {resolve(ok);setTimeout(function () { throw new Error(test) }, 0)
});
promise.then(function (value) { console.log(value) });
// ok
// Uncaught Error: test上面代码中Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候Promise 的运行已经结束了所以这个错误是在 Promise 函数体外抛出的会冒泡到最外层成了未捕获的错误。
一般总是建议Promise 对象后面要跟catch()方法这样可以处理 Promise 内部发生的错误。catch()方法返回的还是一个 Promise 对象因此后面还可以接着调用then()方法。
const someAsyncThing function() {return new Promise(function(resolve, reject) {// 下面一行会报错因为x没有声明resolve(x 2);});
};someAsyncThing()
.catch(function(error) {console.log(oh no, error);
})
.then(function() {console.log(carry on);
});
// oh no [ReferenceError: x is not defined]
// carry on上面代码运行完catch()方法指定的回调函数会接着运行后面那个then()方法指定的回调函数。如果没有报错则会跳过catch()方法。
Promise.resolve()
.catch(function(error) {console.log(oh no, error);
})
.then(function() {console.log(carry on);
});
// carry on上面的代码因为没有报错跳过了 catch() 方法直接执行后面的 then() 方法。此时要是 then() 方法里面报错就与前面的 catch() 无关了。
catch() 方法之中还能再抛出错误。
const someAsyncThing function() {return new Promise(function(resolve, reject) {// 下面一行会报错因为x没有声明resolve(x 2);});
};someAsyncThing().then(function() {return someOtherAsyncThing();
}).catch(function(error) {console.log(oh no, error);// 下面一行会报错因为 y 没有声明y 2;
}).then(function() {console.log(carry on);
});
// oh no [ReferenceError: x is not defined]上面代码中catch()方法抛出一个错误因为后面没有别的catch()方法了导致这个错误不会被捕获也不会传递到外层。如果改写一下结果就不一样了。
someAsyncThing().then(function() {return someOtherAsyncThing();
}).catch(function(error) {console.log(oh no, error);// 下面一行会报错因为y没有声明y 2;
}).catch(function(error) {console.log(carry on, error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]上面代码中第二个catch()方法用来捕获前一个catch()方法抛出的错误。
5. Promise.prototype.finally()
finally()方法用于指定不管 Promise 对象最后状态如何都会执行的操作。该方法是 ES2018 引入标准的。
promise
.then(result {···})
.catch(error {···})
.finally(() {···});上面代码中不管 promise 最后的状态在执行完 then 或 catch 指定的回调函数以后都会执行 finally 方法指定的回调函数。
下面是一个例子服务器使用 Promise 处理请求然后使用 finally 方法关掉服务器。
server.listen(port).then(function () {// ...}).finally(server.stop);finally 方法的回调函数不接受任何参数这意味着没有办法知道前面的 Promise 状态到底是 fulfilled 还是 rejected。这表明finally 方法里面的操作应该是与状态无关的不依赖于 Promise 的执行结果。
finally 本质上是 then 方法的特例。
promise
.finally(() {// 语句
});// 等同于
promise
.then(result {// 语句return result;},error {// 语句throw error;}
);上面代码中如果不使用finally方法同样的语句需要为成功和失败两种情况各写一次。有了finally方法则只需要写一次。
它的实现也很简单。
Promise.prototype.finally function (callback) {let P this.constructor;return this.then(value P.resolve(callback()).then(() value),reason P.resolve(callback()).then(() { throw reason }));
};上面代码中不管前面的 Promise 是fulfilled还是rejected都会执行回调函数callback。
从上面的实现还可以看到finally方法总是会返回原来的值。
// resolve 的值是 undefined
Promise.resolve(2).then(() {}, () {})// resolve 的值是 2
Promise.resolve(2).finally(() {})// reject 的值是 undefined
Promise.reject(3).then(() {}, () {})// reject 的值是 3
Promise.reject(3).finally(() {})6. Promise.all()
Promise.all()方法用于将多个 Promise 实例包装成一个新的 Promise 实例。
const p Promise.all([p1, p2, p3]);上面代码中Promise.all() 方法接受一个数组作为参数p1、p2、p3 都是 Promise 实例如果不是就会先调用下面讲到的 Promise.resolve 方法将参数转为 Promise 实例再进一步处理。另外Promise.all() 方法的参数可以不是数组但必须具有 Iterator 接口且返回的每个成员都是 Promise 实例。
p 的状态由 p1、p2、p3 决定分成两种情况。 只有 p1、p2、p3 的状态都变成 fulfilledp 的状态才会变成 fulfilled此时 p1、p2、p3 的返回值组成一个数组传递给 p 的回调函数。 只要 p1、p2、p3 之中有一个被 rejectedp 的状态就变成 rejected此时第一个被 reject 的实例的返回值会传递给 p 的回调函数。
下面是一个具体的例子。
// 生成一个Promise对象的数组
const promises [2, 3, 5, 7, 11, 13].map(function (id) {return getJSON(/post/ id .json);
});Promise.all(promises).then(function (posts) {// ...
}).catch(function(reason){// ...
});上面代码中promises是包含 6 个 Promise 实例的数组只有这 6 个实例的状态都变成fulfilled或者其中有一个变为rejected才会调用Promise.all方法后面的回调函数。
下面是另一个例子。
const databasePromise connectDatabase();const booksPromise databasePromise.then(findAllBooks);const userPromise databasePromise.then(getCurrentUser);Promise.all([booksPromise,userPromise
])
.then(([books, user]) pickTopRecommendations(books, user));上面代码中booksPromise和userPromise是两个异步操作只有等到它们的结果都返回了才会触发pickTopRecommendations这个回调函数。
注意如果作为参数的 Promise 实例自己定义了catch方法那么它一旦被rejected并不会触发Promise.all()的catch方法。
const p1 new Promise((resolve, reject) {resolve(hello);
})
.then(result result)
.catch(e e);const p2 new Promise((resolve, reject) {throw new Error(报错了);
})
.then(result result)
.catch(e e);Promise.all([p1, p2])
.then(result console.log(result))
.catch(e console.log(e));
// [hello, Error: 报错了]上面代码中p1会resolvedp2首先会rejected但是p2有自己的catch方法该方法返回的是一个新的 Promise 实例p2指向的实际上是这个实例。该实例执行完catch方法后也会变成resolved导致Promise.all()方法参数里面的两个实例都会resolved因此会调用then方法指定的回调函数而不会调用catch方法指定的回调函数。
如果p2没有自己的catch方法就会调用Promise.all()的catch方法。
const p1 new Promise((resolve, reject) {resolve(hello);
})
.then(result result);const p2 new Promise((resolve, reject) {throw new Error(报错了);
})
.then(result result);Promise.all([p1, p2])
.then(result console.log(result))
.catch(e console.log(e));
// Error: 报错了7. Promise.race()
Promise.race()方法同样是将多个 Promise 实例包装成一个新的 Promise 实例。
const p Promise.race([p1, p2, p3]);上面代码中只要p1、p2、p3之中有一个实例率先改变状态p的状态就跟着改变。那个率先改变的 Promise 实例的返回值就传递给p的回调函数。
Promise.race()方法的参数与Promise.all()方法一样如果不是 Promise 实例就会先调用下面讲到的Promise.resolve()方法将参数转为 Promise 实例再进一步处理。
下面是一个例子如果指定时间内没有获得结果就将 Promise 的状态变为reject否则变为resolve。
const p Promise.race([fetch(/resource-that-may-take-a-while),new Promise(function (resolve, reject) {setTimeout(() reject(new Error(request timeout)), 5000)})
]);p
.then(console.log)
.catch(console.error);上面代码中如果 5 秒之内fetch方法无法返回结果变量p的状态就会变为rejected从而触发catch方法指定的回调函数。
8. Promise.allSettled()
有时候我们希望等到一组异步操作都结束了不管每一个操作是成功还是失败再进行下一步操作。但是现有的 Promise 方法很难实现这个要求。
Promise.all()方法只适合所有异步操作都成功的情况如果有一个操作失败就无法满足要求。
const urls [url_1, url_2, url_3];
const requests urls.map(x fetch(x));try {await Promise.all(requests);console.log(所有请求都成功。);
} catch {console.log(至少一个请求失败其他请求可能还没结束。);
}上面示例中Promise.all()可以确定所有请求都成功了但是只要有一个请求失败它就会报错而不管另外的请求是否结束。
为了解决这个问题ES2020 引入了Promise.allSettled()方法用来确定一组异步操作是否都结束了不管成功或失败。所以它的名字叫做”Settled“包含了”fulfilled“和”rejected“两种情况。
Promise.allSettled()方法接受一个数组作为参数数组的每个成员都是一个 Promise 对象并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更不管是fulfilled还是rejected返回的 Promise 对象才会发生状态变更。
const promises [fetch(/api-1),fetch(/api-2),fetch(/api-3),
];await Promise.allSettled(promises);
removeLoadingIndicator();上面示例中数组promises包含了三个请求只有等到这三个请求都结束了不管请求成功还是失败removeLoadingIndicator()才会执行。
该方法返回的新的 Promise 实例一旦发生状态变更状态总是fulfilled不会变成rejected。状态变成fulfilled后它的回调函数会接收到一个数组作为参数该数组的每个成员对应前面数组的每个 Promise 对象。
const resolved Promise.resolve(42);
const rejected Promise.reject(-1);const allSettledPromise Promise.allSettled([resolved, rejected]);allSettledPromise.then(function (results) {console.log(results);
});
// [
// { status: fulfilled, value: 42 },
// { status: rejected, reason: -1 }
// ]上面代码中Promise.allSettled()的返回值allSettledPromise状态只可能变成fulfilled。它的回调函数接收到的参数是数组results。该数组的每个成员都是一个对象对应传入Promise.allSettled()的数组里面的两个 Promise 对象。
results的每个成员是一个对象对象的格式是固定的对应异步操作的结果。
// 异步操作成功时
{status: fulfilled, value: value}// 异步操作失败时
{status: rejected, reason: reason}成员对象的status属性的值只可能是字符串fulfilled或字符串rejected用来区分异步操作是成功还是失败。如果是成功fulfilled对象会有value属性如果是失败rejected会有reason属性对应两种状态时前面异步操作的返回值。
下面是返回值的用法例子。
const promises [ fetch(index.html), fetch(https://does-not-exist/) ];
const results await Promise.allSettled(promises);// 过滤出成功的请求
const successfulPromises results.filter(p p.status fulfilled);// 过滤出失败的请求并输出原因
const errors results.filter(p p.status rejected).map(p p.reason);9. Promise.any()
ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数包装成一个新的 Promise 实例返回。
Promise.any([fetch(https://v8.dev/).then(() home),fetch(https://v8.dev/blog).then(() blog),fetch(https://v8.dev/docs).then(() docs)
]).then((first) { // 只要有一个 fetch() 请求成功console.log(first);
}).catch((error) { // 所有三个 fetch() 全部请求失败console.log(error);
});只要参数实例有一个变成fulfilled状态包装实例就会变成fulfilled状态如果所有参数实例都变成rejected状态包装实例就会变成rejected状态。
Promise.any()跟Promise.race()方法很像只有一点不同就是Promise.any()不会因为某个 Promise 变成rejected状态而结束必须等到所有参数 Promise 变成rejected状态才会结束。
下面是Promise()与await命令结合使用的例子。
const promises [fetch(/endpoint-a).then(() a),fetch(/endpoint-b).then(() b),fetch(/endpoint-c).then(() c),
];try {const first await Promise.any(promises);console.log(first);
} catch (error) {console.log(error);上面代码中Promise.any()方法的参数数组包含三个 Promise 操作。其中只要有一个变成fulfilledPromise.any()返回的 Promise 对象就变成fulfilled。如果所有三个操作都变成rejected那么await命令就会抛出错误。
Promise.any()抛出的错误是一个 AggregateError 实例详见《对象的扩展》一章这个 AggregateError 实例对象的errors属性是一个数组包含了所有成员的错误。
下面是一个例子。
var resolved Promise.resolve(42);
var rejected Promise.reject(-1);
var alsoRejected Promise.reject(Infinity);Promise.any([resolved, rejected, alsoRejected]).then(function (result) {console.log(result); // 42
});Promise.any([rejected, alsoRejected]).catch(function (results) {console.log(results instanceof AggregateError); // trueconsole.log(results.errors); // [-1, Infinity]
});10. Promise.resolve()
有时需要将现有对象转为 Promise 对象Promise.resolve()方法就起到这个作用。
const jsPromise Promise.resolve($.ajax(/whatever.json));上面代码将 jQuery 生成的deferred对象转为一个新的 Promise 对象。
Promise.resolve()等价于下面的写法。
Promise.resolve(foo)
// 等价于
new Promise(resolve resolve(foo))Promise.resolve()方法的参数分成四种情况。
1参数是一个 Promise 实例
如果参数是 Promise 实例那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
2参数是一个 thenable 对象
thenable对象指的是具有then方法的对象比如下面这个对象。
let thenable {then: function(resolve, reject) {resolve(42);}
};Promise.resolve()方法会将这个对象转为 Promise 对象然后就立即执行thenable对象的then()方法。
let thenable {then: function(resolve, reject) {resolve(42);}
};let p1 Promise.resolve(thenable);
p1.then(function (value) {console.log(value); // 42
});上面代码中thenable对象的then()方法执行后对象p1的状态就变为resolved从而立即执行最后那个then()方法指定的回调函数输出42。
3参数不是具有 then() 方法的对象或根本就不是对象
如果参数是一个原始值或者是一个不具有then()方法的对象则Promise.resolve()方法返回一个新的 Promise 对象状态为resolved。
const p Promise.resolve(Hello);p.then(function (s) {console.log(s)
});
// Hello上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作判断方法是字符串对象不具有 then 方法返回 Promise 实例的状态从一生成就是resolved所以回调函数会立即执行。Promise.resolve()方法的参数会同时传给回调函数。
4不带有任何参数
Promise.resolve()方法允许调用时不带参数直接返回一个resolved状态的 Promise 对象。
所以如果希望得到一个 Promise 对象比较方便的方法就是直接调用Promise.resolve()方法。
const p Promise.resolve();p.then(function () {// ...
});上面代码的变量p就是一个 Promise 对象。
需要注意的是立即resolve()的 Promise 对象是在本轮“事件循环”event loop的结束时执行而不是在下一轮“事件循环”的开始时。
setTimeout(function () {console.log(three);
}, 0);Promise.resolve().then(function () {console.log(two);
});console.log(one);// one
// two
// three上面代码中setTimeout(fn, 0)在下一轮“事件循环”开始时执行Promise.resolve()在本轮“事件循环”结束时执行console.log(one)则是立即执行因此最先输出。
11. Promise.reject()
Promise.reject(reason)方法也会返回一个新的 Promise 实例该实例的状态为rejected。
const p Promise.reject(出错了);
// 等同于
const p new Promise((resolve, reject) reject(出错了))p.then(null, function (s) {console.log(s)
});
// 出错了上面代码生成一个 Promise 对象的实例p状态为rejected回调函数会立即执行。
Promise.reject()方法的参数会原封不动地作为reject的理由变成后续方法的参数。
Promise.reject(出错了)
.catch(e {console.log(e 出错了)
})
// true上面代码中Promise.reject()方法的参数是一个字符串后面catch()方法的参数e就是这个字符串。
12. 应用
加载图片
我们可以将图片的加载写成一个Promise一旦加载完成Promise的状态就发生变化。
const preloadImage function (path) {return new Promise(function (resolve, reject) {const image new Image();image.onload resolve;image.onerror reject;image.src path;});
};Generator 函数与 Promise 的结合
使用 Generator 函数管理流程遇到异步操作的时候通常返回一个Promise对象。
function getFoo () {return new Promise(function (resolve, reject){resolve(foo);});
}const g function* () {try {const foo yield getFoo();console.log(foo);} catch (e) {console.log(e);}
};function run (generator) {const it generator();function go(result) {if (result.done) return result.value;return result.value.then(function (value) {return go(it.next(value));}, function (error) {return go(it.throw(error));});}go(it.next());
}run(g);上面代码的 Generator 函数 g 之中有一个异步操作 getFoo它返回的就是一个 Promise 对象。函数 run 用来处理这个 Promise 对象并调用下一个 next 方法。
13. Promise.try()
实际开发中经常遇到一种情况不知道或者不想区分函数f是同步函数还是异步操作但是想用 Promise 来处理它。因为这样就可以不管f是否包含异步操作都用then方法指定下一步流程用catch方法处理f抛出的错误。一般就会采用下面的写法。
Promise.resolve().then(f)上面的写法有一个缺点就是如果f是同步函数那么它会在本轮事件循环的末尾执行。
const f () console.log(now);
Promise.resolve().then(f);
console.log(next);
// next
// now上面代码中函数f是同步的但是用 Promise 包装了以后就变成异步执行了。
那么有没有一种方法让同步函数同步执行异步函数异步执行并且让它们具有统一的 API 呢回答是可以的并且还有两种写法。第一种写法是用async函数来写。
const f () console.log(now);
(async () f())();
console.log(next);
// now
// next上面代码中第二行是一个立即执行的匿名函数会立即执行里面的 async 函数因此如果 f 是同步的就会得到同步的结果如果 f 是异步的就可以用 then 指定下一步就像下面的写法。
(async () f())()
.then(...)需要注意的是async () f() 会吃掉 f() 抛出的错误。所以如果想捕获错误要使用 promise.catch 方法。
(async () f())()
.then(...)
.catch(...)第二种写法是使用new Promise()。
const f () console.log(now);
(() new Promise(resolve resolve(f()))
)();
console.log(next);
// now
// next上面代码也是使用立即执行的匿名函数执行new Promise()。这种情况下同步函数也是同步执行的。
鉴于这是一个很常见的需求所以现在有一个提案提供Promise.try方法替代上面的写法。
const f () console.log(now);
Promise.try(f);
console.log(next);
// now
// next事实上Promise.try 存在已久Promise 库 Bluebird、Q 和 when早就提供了这个方法。
由于 Promise.try 为所有操作提供了统一的处理机制所以如果想用 then 方法管理流程最好都用 Promise.try 包装一下。这样有许多好处其中一点就是可以更好地管理异常。
function getUsername(userId) {return database.users.get({id: userId}).then(function(user) {return user.name;});
}上面代码中database.users.get()返回一个 Promise 对象如果抛出异步错误可以用catch方法捕获就像下面这样写。
database.users.get({id: userId})
.then(...)
.catch(...)但是database.users.get()可能还会抛出同步错误比如数据库连接错误具体要看实现方法这时你就不得不用try...catch去捕获。
try {database.users.get({id: userId}).then(...).catch(...)
} catch (e) {// ...
}上面这样的写法就很笨拙了这时就可以统一用promise.catch()捕获所有同步和异步的错误。
Promise.try(() database.users.get({id: userId})).then(...).catch(...)事实上Promise.try就是模拟try代码块就像promise.catch模拟的是catch代码块。
async 函数
1. 含义
ES2017 标准引入了 async 函数使得异步操作变得更加方便。
async 函数是什么一句话它就是 Generator 函数的语法糖。
前文有一个 Generator 函数依次读取两个文件。
const fs require(fs);const readFile function (fileName) {return new Promise(function (resolve, reject) {fs.readFile(fileName, function(error, data) {if (error) return reject(error);resolve(data);});});
};const gen function* () {const f1 yield readFile(/etc/fstab);const f2 yield readFile(/etc/shells);console.log(f1.toString());console.log(f2.toString());
};上面代码的函数gen可以写成async函数就是下面这样。
const asyncReadFile async function () {const f1 await readFile(/etc/fstab);const f2 await readFile(/etc/shells);console.log(f1.toString());console.log(f2.toString());
};一比较就会发现async 函数就是将 Generator 函数的星号*替换成 async将 yield 替换成 await仅此而已。
async 函数对 Generator 函数的改进体现在以下四点。
1内置执行器。
Generator 函数的执行必须靠执行器所以才有了 co 模块而 async 函数自带执行器。也就是说async 函数的执行与普通函数一模一样只要一行。
asyncReadFile();上面的代码调用了asyncReadFile函数然后它就会自动执行输出最后结果。这完全不像 Generator 函数需要调用next方法或者用co模块才能真正执行得到最后结果。
2更好的语义。
async 和 await比起星号和 yield语义更清楚了。async 表示函数里有异步操作await 表示紧跟在后面的表达式需要等待结果。
3更广的适用性。
co 模块约定yield 命令后面只能是 Thunk 函数或 Promise 对象而 async 函数的 await 命令后面可以是 Promise 对象和原始类型的值数值、字符串和布尔值但这时会自动转成立即 resolved 的 Promise 对象。
4返回值是 Promise。
async 函数的返回值是 Promise 对象这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用 then 方法指定下一步的操作。
进一步说async 函数完全可以看作多个异步操作包装成的一个 Promise 对象而 await 命令就是内部 then 命令的语法糖。
2. 基本用法
async 函数返回一个 Promise 对象可以使用 then 方法添加回调函数。当函数执行的时候一旦遇到 await 就会先返回等到异步操作完成再接着执行函数体内后面的语句。
下面是一个例子。
async function getStockPriceByName(name) {const symbol await getStockSymbol(name);const stockPrice await getStockPrice(symbol);return stockPrice;
}getStockPriceByName(goog).then(function (result) {console.log(result);
});上面代码是一个获取股票报价的函数函数前面的async关键字表明该函数内部有异步操作。调用该函数时会立即返回一个Promise对象。
下面是另一个例子指定多少毫秒后输出一个值。
function timeout(ms) {return new Promise((resolve) {setTimeout(resolve, ms);});
}async function asyncPrint(value, ms) {await timeout(ms);console.log(value);
}asyncPrint(hello world, 50);上面代码指定 50 毫秒以后输出hello world。
由于async函数返回的是 Promise 对象可以作为await命令的参数。所以上面的例子也可以写成下面的形式。
async function timeout(ms) {await new Promise((resolve) {setTimeout(resolve, ms);});
}async function asyncPrint(value, ms) {await timeout(ms);console.log(value);
}asyncPrint(hello world, 50);async 函数有多种使用形式。
// 函数声明
async function foo() {}// 函数表达式
const foo async function () {};// 对象的方法
let obj { async foo() {} };
obj.foo().then(...)// Class 的方法
class Storage {constructor() {this.cachePromise caches.open(avatars);}async getAvatar(name) {const cache await this.cachePromise;return cache.match(/avatars/${name}.jpg);}
}const storage new Storage();
storage.getAvatar(jake).then(…);// 箭头函数
const foo async () {};3. 语法
async函数的语法规则总体上比较简单难点是错误处理机制。
返回 Promise 对象
async函数返回一个 Promise 对象。
async函数内部return语句返回的值会成为then方法回调函数的参数。
async function f() {return hello world;
}f().then(v console.log(v))
// hello world上面代码中函数f内部return命令返回的值会被then方法回调函数接收到。
async函数内部抛出错误会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。
async function f() {throw new Error(出错了);
}f().then(v console.log(resolve, v),e console.log(reject, e)
)
//reject Error: 出错了Promise 对象的状态变化
async函数返回的 Promise 对象必须等到内部所有await命令后面的 Promise 对象执行完才会发生状态改变除非遇到return语句或者抛出错误。也就是说只有async函数内部的异步操作执行完才会执行then方法指定的回调函数。
下面是一个例子。
async function getTitle(url) {let response await fetch(url);let html await response.text();return html.match(/title([\s\S])\/title/i)[1];
}
getTitle(https://tc39.github.io/ecma262/).then(console.log)
// ECMAScript 2017 Language Specification上面代码中函数getTitle内部有三个操作抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成才会执行then方法里面的console.log。
await 命令
正常情况下await命令后面是一个 Promise 对象返回该对象的结果。如果不是 Promise 对象就直接返回对应的值。
async function f() {// 等同于// return 123;return await 123;
}f().then(v console.log(v))
// 123上面代码中await命令的参数是数值123这时等同于return 123。
另一种情况是await命令后面是一个thenable对象即定义了then方法的对象那么await会将其等同于 Promise 对象。
class Sleep {constructor(timeout) {this.timeout timeout;}then(resolve, reject) {const startTime Date.now();setTimeout(() resolve(Date.now() - startTime),this.timeout);}
}(async () {const sleepTime await new Sleep(1000);console.log(sleepTime);
})();
// 1000上面代码中await命令后面是一个Sleep对象的实例。这个实例不是 Promise 对象但是因为定义了then方法await会将其视为 Promise 处理。
这个例子还演示了如何实现休眠效果。JavaScript 一直没有休眠的语法但是借助await命令就可以让程序停顿指定的时间。下面给出了一个简化的sleep实现。
function sleep(interval) {return new Promise(resolve {setTimeout(resolve, interval);})
}// 用法
async function one2FiveInAsync() {for(let i 1; i 5; i) {console.log(i);await sleep(1000);}
}one2FiveInAsync();await命令后面的 Promise 对象如果变为reject状态则reject的参数会被catch方法的回调函数接收到。
async function f() {await Promise.reject(出错了);
}f()
.then(v console.log(v))
.catch(e console.log(e))
// 出错了注意上面代码中await语句前面没有return但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return效果是一样的。
任何一个await语句后面的 Promise 对象变为reject状态那么整个async函数都会中断执行。
async function f() {await Promise.reject(出错了);await Promise.resolve(hello world); // 不会执行
}上面代码中第二个await语句是不会执行的因为第一个await语句状态变成了reject。
有时我们希望即使前一个异步操作失败也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面这样不管这个异步操作是否成功第二个await都会执行。
async function f() {try {await Promise.reject(出错了);} catch(e) {}return await Promise.resolve(hello world);
}f()
.then(v console.log(v))
// hello world另一种方法是await后面的 Promise 对象再跟一个catch方法处理前面可能出现的错误。
async function f() {await Promise.reject(出错了).catch(e console.log(e));return await Promise.resolve(hello world);
}f()
.then(v console.log(v))
// 出错了
// hello world错误处理
如果await后面的异步操作出错那么等同于async函数返回的 Promise 对象被reject。
async function f() {await new Promise(function (resolve, reject) {throw new Error(出错了);});
}f()
.then(v console.log(v))
.catch(e console.log(e))
// Error出错了上面代码中async函数f执行后await后面的 Promise 对象会抛出一个错误对象导致catch方法的回调函数被调用它的参数就是抛出的错误对象。具体的执行机制可以参考后文的“async 函数的实现原理”。
防止出错的方法也是将其放在try...catch代码块之中。
async function f() {try {await new Promise(function (resolve, reject) {throw new Error(出错了);});} catch(e) {}return await(hello world);
}如果有多个await命令可以统一放在try...catch结构中。
async function main() {try {const val1 await firstStep();const val2 await secondStep(val1);const val3 await thirdStep(val1, val2);console.log(Final: , val3);}catch (err) {console.error(err);}
}下面的例子使用try...catch结构实现多次重复尝试。
const superagent require(superagent);
const NUM_RETRIES 3;async function test() {let i;for (i 0; i NUM_RETRIES; i) {try {await superagent.get(http://google.com/this-throws-an-error);break;} catch(err) {}}console.log(i); // 3
}test();上面代码中如果await操作成功就会使用break语句退出循环如果失败会被catch语句捕捉然后进入下一轮循环。
使用注意点
第一点前面已经说过await命令后面的Promise对象运行结果可能是rejected所以最好把await命令放在try...catch代码块中。
async function myFunction() {try {await somethingThatReturnsAPromise();} catch (err) {console.log(err);}
}// 另一种写法async function myFunction() {await somethingThatReturnsAPromise().catch(function (err) {console.log(err);});
}第二点多个await命令后面的异步操作如果不存在继发关系最好让它们同时触发。
let foo await getFoo();
let bar await getBar();上面代码中getFoo 和 getBar 是两个独立的异步操作即互不依赖被写成继发关系。这样比较耗时因为只有 getFoo 完成以后才会执行 getBar完全可以让它们同时触发。
// 写法一
let [foo, bar] await Promise.all([getFoo(), getBar()]);// 写法二
let fooPromise getFoo();
let barPromise getBar();
let foo await fooPromise;
let bar await barPromise;上面两种写法getFoo 和 getBar 都是同时触发这样就会缩短程序的执行时间。
第三点await命令只能用在async函数之中如果用在普通函数就会报错。
async function dbFuc(db) {let docs [{}, {}, {}];// 报错docs.forEach(function (doc) {await db.post(doc);});
}上面代码会报错因为await用在普通函数之中了。但是如果将forEach方法的参数改成async函数也有问题。
function dbFuc(db) { //这里不需要 asynclet docs [{}, {}, {}];// 可能得到错误结果docs.forEach(async function (doc) {await db.post(doc);});
}上面代码可能不会正常工作原因是这时三个db.post()操作将是并发执行也就是同时执行而不是继发执行。正确的写法是采用for循环。
async function dbFuc(db) {let docs [{}, {}, {}];for (let doc of docs) {await db.post(doc);}
}另一种方法是使用数组的reduce()方法。
async function dbFuc(db) {let docs [{}, {}, {}];await docs.reduce(async (_, doc) {await _;await db.post(doc);}, undefined);
}上面例子中reduce()方法的第一个参数是async函数导致该函数的第一个参数是前一步操作返回的 Promise 对象所以必须使用await等待它操作结束。另外reduce()方法返回的是docs数组最后一个成员的async函数的执行结果也是一个 Promise 对象导致在它前面也必须加上await。
上面的reduce()的参数函数里面没有return语句原因是这个函数的主要目的是db.post()操作不是返回值。而且async函数不管有没有return语句总是返回一个 Promise 对象所以这里的return是不必要的。
如果确实希望多个请求并发执行可以使用Promise.all方法。当三个请求都会resolved时下面两种写法效果相同。
async function dbFuc(db) {let docs [{}, {}, {}];let promises docs.map((doc) db.post(doc));let results await Promise.all(promises);console.log(results);
}// 或者使用下面的写法async function dbFuc(db) {let docs [{}, {}, {}];let promises docs.map((doc) db.post(doc));let results [];for (let promise of promises) {results.push(await promise);}console.log(results);
}第四点async 函数可以保留运行堆栈。
const a () {b().then(() c());
};上面代码中函数a内部运行了一个异步任务b()。当b()运行的时候函数a()不会中断而是继续执行。等到b()运行结束可能a()早就运行结束了b()所在的上下文环境已经消失了。如果b()或c()报错错误堆栈将不包括a()。
现在将这个例子改成async函数。
const a async () {await b();c();
};上面代码中b()运行的时候a()是暂停执行上下文环境都保存着。一旦b()或c()报错错误堆栈将包括a()。
4. async 函数的实现原理
async 函数的实现原理就是将 Generator 函数和自动执行器包装在一个函数里。
async function fn(args) {// ...
}// 等同于function fn(args) {return spawn(function* () {// ...});
}所有的async函数都可以写成上面的第二种形式其中的spawn函数就是自动执行器。
下面给出spawn函数的实现基本就是前文自动执行器的翻版。
function spawn(genF) {return new Promise(function(resolve, reject) {const gen genF();function step(nextF) {let next;try {next nextF();} catch(e) {return reject(e);}if(next.done) {return resolve(next.value);}Promise.resolve(next.value).then(function(v) {step(function() { return gen.next(v); });}, function(e) {step(function() { return gen.throw(e); });});}step(function() { return gen.next(undefined); });});
}5. 与其他异步处理方法的比较
我们通过一个例子来看 async 函数与 Promise、Generator 函数的比较。
假定某个 DOM 元素上面部署了一系列的动画前一个动画结束才能开始后一个。如果当中有一个动画出错就不再往下执行返回上一个成功执行的动画的返回值。
首先是 Promise 的写法。
function chainAnimationsPromise(elem, animations) {// 变量ret用来保存上一个动画的返回值let ret null;// 新建一个空的Promiselet p Promise.resolve();// 使用then方法添加所有动画for(let anim of animations) {p p.then(function(val) {ret val;return anim(elem);});}// 返回一个部署了错误捕捉机制的Promisereturn p.catch(function(e) {/* 忽略错误继续执行 */}).then(function() {return ret;});}虽然 Promise 的写法比回调函数的写法大大改进但是一眼看上去代码完全都是 Promise 的 APIthen、catch等等操作本身的语义反而不容易看出来。
接着是 Generator 函数的写法。
function chainAnimationsGenerator(elem, animations) {return spawn(function*() {let ret null;try {for(let anim of animations) {ret yield anim(elem);}} catch(e) {/* 忽略错误继续执行 */}return ret;});}上面代码使用 Generator 函数遍历了每个动画语义比 Promise 写法更清晰用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于必须有一个任务运行器自动执行 Generator 函数上面代码的spawn函数就是自动执行器它返回一个 Promise 对象而且必须保证yield语句后面的表达式必须返回一个 Promise。
最后是 async 函数的写法。
async function chainAnimationsAsync(elem, animations) {let ret null;try {for(let anim of animations) {ret await anim(elem);}} catch(e) {/* 忽略错误继续执行 */}return ret;
}可以看到 Async 函数的实现最简洁最符合语义几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器改在语言层面提供不暴露给用户因此代码量最少。如果使用 Generator 写法自动执行器需要用户自己提供。
6. 实例按顺序完成异步操作
实际开发中经常遇到一组异步操作需要按照顺序完成。比如依次远程读取一组 URL然后按照读取的顺序输出结果。
Promise 的写法如下。
function logInOrder(urls) {// 远程读取所有URLconst textPromises urls.map(url {return fetch(url).then(response response.text());});// 按次序输出textPromises.reduce((chain, textPromise) {return chain.then(() textPromise).then(text console.log(text));}, Promise.resolve());
}上面代码使用fetch方法同时远程读取一组 URL。每个fetch操作都返回一个 Promise 对象放入textPromises数组。然后reduce方法依次处理每个 Promise 对象然后使用then将所有 Promise 对象连起来因此就可以依次输出结果。
这种写法不太直观可读性比较差。下面是 async 函数实现。
async function logInOrder(urls) {for (const url of urls) {const response await fetch(url);console.log(await response.text());}
}上面代码确实大大简化问题是所有远程操作都是继发。只有前一个 URL 返回结果才会去读取下一个 URL这样做效率很差非常浪费时间。我们需要的是并发发出远程请求。
async function logInOrder(urls) {// 并发读取远程URLconst textPromises urls.map(async url {const response await fetch(url);return response.text();});// 按次序输出for (const textPromise of textPromises) {console.log(await textPromise);}
}上面代码中虽然map方法的参数是async函数但它是并发执行的因为只有async函数内部是继发执行外部不受影响。后面的for..of循环内部使用了await因此实现了按顺序输出。
7. 顶层 await
早期的语法规定是await命令只能出现在 async 函数内部否则都会报错。
// 报错
const data await fetch(https://api.example.com);上面代码中await命令独立使用没有放在 async 函数里面就会报错。
从 ES2022 开始允许在模块的顶层独立使用await命令使得上面那行代码不会报错了。它的主要目的是使用await解决模块异步加载的问题。
// awaiting.js
let output;
async function main() {const dynamic await import(someMission);const data await fetch(url);output someProcess(dynamic.default, data);
}
main();
export { output };上面代码中模块awaiting.js的输出值output取决于异步操作。我们把异步操作包装在一个 async 函数里面然后调用这个函数只有等里面的异步操作都执行变量output才会有值否则就返回undefined。
下面是加载这个模块的写法。
// usage.js
import { output } from ./awaiting.js;function outputPlusValue(value) { return output value }console.log(outputPlusValue(100));
setTimeout(() console.log(outputPlusValue(100)), 1000);上面代码中outputPlusValue()的执行结果完全取决于执行的时间。如果awaiting.js里面的异步操作没执行完加载进来的output的值就是undefined。
目前的解决方法就是让原始模块输出一个 Promise 对象从这个 Promise 对象判断异步操作有没有结束。
// awaiting.js
let output;
export default (async function main() {const dynamic await import(someMission);const data await fetch(url);output someProcess(dynamic.default, data);
})();
export { output };上面代码中awaiting.js除了输出output还默认输出一个 Promise 对象async 函数立即执行后返回一个 Promise 对象从这个对象判断异步操作是否结束。
下面是加载这个模块的新的写法。
// usage.js
import promise, { output } from ./awaiting.js;function outputPlusValue(value) { return output value }promise.then(() {console.log(outputPlusValue(100));setTimeout(() console.log(outputPlusValue(100)), 1000);
});上面代码中将awaiting.js对象的输出放在promise.then()里面这样就能保证异步操作完成以后才去读取output。
这种写法比较麻烦等于要求模块的使用者遵守一个额外的使用协议按照特殊的方法使用这个模块。一旦你忘了要用 Promise 加载只使用正常的加载方法依赖这个模块的代码就可能出错。而且如果上面的usage.js又有对外的输出等于这个依赖链的所有模块都要使用 Promise 加载。
顶层的await命令就是为了解决这个问题。它保证只有异步操作完成模块才会输出值。
// awaiting.js
const dynamic import(someMission);
const data fetch(url);
export const output someProcess((await dynamic).default, await data);上面代码中两个异步操作在输出的时候都加上了await命令。只有等到异步操作完成这个模块才会输出值。
加载这个模块的写法如下。
// usage.js
import { output } from ./awaiting.js;
function outputPlusValue(value) { return output value }console.log(outputPlusValue(100));
setTimeout(() console.log(outputPlusValue(100)), 1000);上面代码的写法与普通的模块加载完全一样。也就是说模块的使用者完全不用关心依赖模块的内部有没有异步操作正常加载即可。
这时模块的加载会等待依赖模块上例是awaiting.js的异步操作完成才执行后面的代码有点像暂停在那里。所以它总是会得到正确的output不会因为加载时机的不同而得到不一样的值。
注意顶层await只能用在 ES6 模块不能用在 CommonJS 模块。这是因为 CommonJS 模块的require()是同步加载如果有顶层await就没法处理加载了。
下面是顶层await的一些使用场景。
// import() 方法加载
const strings await import(/i18n/${navigator.language});// 数据库操作
const connection await dbConnector();// 依赖回滚
let jQuery;
try {jQuery await import(https://cdn-a.com/jQuery);
} catch {jQuery await import(https://cdn-b.com/jQuery);
}注意如果加载多个包含顶层await命令的模块加载命令是同步执行的。
// x.js
console.log(X1);
await new Promise(r setTimeout(r, 1000));
console.log(X2);// y.js
console.log(Y);// z.js
import ./x.js;
import ./y.js;
console.log(Z);上面代码有三个模块最后的z.js加载x.js和y.js打印结果是X1、Y、X2、Z。这说明z.js并没有等待x.js加载完成再去加载y.js。
顶层的await命令有点像交出代码的执行权给其他的模块加载等异步操作完成后再拿回执行权继续向下执行。