顶层 await

根据语法规格,await命令只能出现在 async 函数内部,否则都会报错。

  1. // 报错
  2. const data = await fetch('https://api.example.com');

上面代码中,await命令独立使用,没有放在 async 函数里面,就会报错。

目前,有一个语法提案,允许在模块的顶层独立使用await命令,使得上面那行代码不会报错了。这个提案的目的,是借用await解决模块异步加载的问题。

  1. // awaiting.js
  2. let output;
  3. async function main() {
  4. const dynamic = await import(someMission);
  5. const data = await fetch(url);
  6. output = someProcess(dynamic.default, data);
  7. }
  8. main();
  9. export { output };

上面代码中,模块awaiting.js的输出值output,取决于异步操作。我们把异步操作包装在一个 async 函数里面,然后调用这个函数,只有等里面的异步操作都执行,变量output才会有值,否则就返回undefined

上面的代码也可以写成立即执行函数的形式。

  1. // awaiting.js
  2. let output;
  3. (async function1 main() {
  4. const dynamic = await import(someMission);
  5. const data = await fetch(url);
  6. output = someProcess(dynamic.default, data);
  7. })();
  8. export { output };

下面是加载这个模块的写法。

  1. // usage.js
  2. import { output } from "./awaiting.js";
  3. function outputPlusValue(value) { return output + value }
  4. console.log(outputPlusValue(100));
  5. setTimeout(() => console.log(outputPlusValue(100), 1000);

上面代码中,outputPlusValue()的执行结果,完全取决于执行的时间。如果awaiting.js里面的异步操作没执行完,加载进来的output的值就是undefined

目前的解决方法,就是让原始模块输出一个 Promise 对象,从这个 Promise 对象判断异步操作有没有结束。

  1. // awaiting.js
  2. let output;
  3. export default (async function main() {
  4. const dynamic = await import(someMission);
  5. const data = await fetch(url);
  6. output = someProcess(dynamic.default, data);
  7. })();
  8. export { output };

上面代码中,awaiting.js除了输出output,还默认输出一个 Promise 对象(async 函数立即执行后,返回一个 Promise 对象),从这个对象判断异步操作是否结束。

下面是加载这个模块的新的写法。

  1. // usage.js
  2. import promise, { output } from "./awaiting.js";
  3. function outputPlusValue(value) { return output + value }
  4. promise.then(() => {
  5. console.log(outputPlusValue(100));
  6. setTimeout(() => console.log(outputPlusValue(100), 1000);
  7. });

上面代码中,将awaiting.js对象的输出,放在promise.then()里面,这样就能保证异步操作完成以后,才去读取output

这种写法比较麻烦,等于要求模块的使用者遵守一个额外的使用协议,按照特殊的方法使用这个模块。一旦你忘了要用 Promise 加载,只使用正常的加载方法,依赖这个模块的代码就可能出错。而且,如果上面的usage.js又有对外的输出,等于这个依赖链的所有模块都要使用 Promise 加载。

顶层的await命令,就是为了解决这个问题。它保证只有异步操作完成,模块才会输出值。

  1. // awaiting.js
  2. const dynamic = import(someMission);
  3. const data = fetch(url);
  4. export const output = someProcess((await dynamic).default, await data);

上面代码中,两个异步操作在输出的时候,都加上了await命令。只有等到异步操作完成,这个模块才会输出值。

加载这个模块的写法如下。

  1. // usage.js
  2. import { output } from "./awaiting.js";
  3. function outputPlusValue(value) { return output + value }
  4. console.log(outputPlusValue(100));
  5. setTimeout(() => console.log(outputPlusValue(100), 1000);

上面代码的写法,与普通的模块加载完全一样。也就是说,模块的使用者完全不用关心,依赖模块的内部有没有异步操作,正常加载即可。

这时,模块的加载会等待依赖模块(上例是awaiting.js)的异步操作完成,才执行后面的代码,有点像暂停在那里。所以,它总是会得到正确的output,不会因为加载时机的不同,而得到不一样的值。

下面是顶层await的一些使用场景。

  1. // import() 方法加载
  2. const strings = await import(`/i18n/${navigator.language}`);
  3. // 数据库操作
  4. const connection = await dbConnector();
  5. // 依赖回滚
  6. let jQuery;
  7. try {
  8. jQuery = await import('https://cdn-a.com/jQuery');
  9. } catch {
  10. jQuery = await import('https://cdn-b.com/jQuery');
  11. }

注意,如果加载多个包含顶层await命令的模块,加载命令是同步执行的。

  1. // x.js
  2. console.log("X1");
  3. await new Promise(r => setTimeout(r, 1000));
  4. console.log("X2");
  5. // y.js
  6. console.log("Y");
  7. // z.js
  8. import "./x.js";
  9. import "./y.js";
  10. console.log("Z");

上面代码有三个模块,最后的z.js加载x.jsy.js,打印结果是X1YX2Z。这说明,z.js并没有等待x.js加载完成,再去加载y.js

顶层的await命令有点像,交出代码的执行权给其他的模块加载,等异步操作完成后,再拿回执行权,继续向下执行。