动态 import & top-level await

动态 import & top-level await

CommonJS 允许你可以在用到的时候再去加载这个模块,而不用全部放到顶部加载。

而 ES Module 的语法是静态的,会自动提升到代码的顶层。

以下面这个 Node 模块为例子,最后依次打印出来的是 main、noop。

// noop.js
console.log('noop');
module.exports = function() {}
// main.js
console.log('main')
const noop = require('./noop')

如果换成 import/export,不管你将 import 放到哪里,打印结果都是相反的。比如下面依次打印的是 noop、main。

// noop.js
console.log('noop');
export default function() {}
// main.js
console.log('main')
import noop from './noop'
动态 import

通过动态 import 允许我们按需加载 JavaScript 模块,而不会在最开始的时候就将全部模块加载。

动态 import 返回了一个 Promise 对象,这也意味着可以在 then 中等模块加载成功后去做一些操作。



Content will load here!
自己实现一个动态的 import

其实我们自己也完全可以通过 Promise 来封装这样一个 api,核心在于动态生成 script 标签。
首先我们返回一个新的 Promise,然后创建一个 script 元素。

在 script 元素的 textContent 里面使用 import 来导入我们想要加载的模块,并将其挂载到 window 上面。

当 script 的 onload 事件触发之时,就把 tempModule 给 resolve 出去,同时删除 window 上面的 tempModule。

最后把 script 元素插入到 document 中,这样就实现了一个动态 import。
这个 importModule 也是官方推荐的在不支持动态 import 的浏览器环境中的一种实现。

function importModule(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
    script.type = "module";
    script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;

    script.onload = () => {
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      script.remove();
    };

    script.onerror = () => {
      reject(new Error("Failed to load module script with URL " + url));
      delete window[tempGlobal];
      script.remove();
    };

    document.documentElement.appendChild(script);
  });
}
Top-level await

如果你认真研究过 ES Module 和 CommonJS,会发现两者在导出值的时候还有一个区别。

可以简单地理解为,CommonJS 导出的是快照,而 ES Module 导出的是引用。

举个栗子:

我们在模块 A 里面定义一个变量 count,将其导出,同时在这个模块中设置 1000ms 之后修改 count 值。

// moduleA.js
export let count = 0;
setTimeout(() => {
    count = 10;
}, 1000)

// moduleB.js
import { count } from 'moduleA'

console.log(count);
setTimeout(() => {
    console.log(count);
}, 2000)

你会觉得这两次输出会有什么不一样吗?这个 count 怎么看都是一个基本类型,难道 2000ms 之后输出还会变化不成?

没错,在 2000ms 后再去打印 count 的确是会变化,你会发现 count 变成了 10,这也意味着 ES Module 导出的时候并不会用快照,而是从引用中来获取值。

而在 CommonJS 中则完全相反,CommonJS 中两次都输出了 0,这意味着 CommonJS 导出的是快照。

IIAFEs 的局限性

已知在 JS 中使用 await 都要在外面套一个 async 函数,如果想要导出一个异步获取之后的值,传统的做法如下:


// awaiting.mjs
import { process } from "./some-module.mjs";
let output;
async function main() {
  const dynamic = await import(computedModuleSpecifier);
  const data = await fetch(url);
  output = process(dynamic.default, data);
}
main();
export { output };

或者使用 IIAFE,由于这种模式和 IFEE 比较像,所以被叫做 Immediately Invoked Async Function Expression,简称 IIAFE。

// awaiting.mjs
import { process } from "./some-module.mjs";
let output;
(async () => {
  const dynamic = await import(computedModuleSpecifier);
  const data = await fetch(url);
  output = process(dynamic.default, data);
})();
export { output };

但是这两种做法有一个问题,如果导入这个模块后立即使用 output,那么拿到的是个 undefined,因为异步加载的数据还没有获取到。一直到异步加载的数据拿到了之后,才能导入正确的值。

想要拿到异步加载之后的数据,最粗暴的方式就是在一段时间之后再去获取这个 output,例如:

import { output } from './awaiting'
setTimeout(() => {
    console.log(output)
}, 2000)
升级版的 IIAFEs

当然上面的这种做法也很不靠谱,毕竟谁也不知道异步加载要经过多少秒才返回,所以就诞生了另外一种写法,直接导出整个 async 函数 和 output 变量。

// awaiting.mjs
import { process } from "./some-module.mjs";
let output;
export default (async () => {
  const dynamic = await import(computedModuleSpecifier);
  const data = await fetch(url);
  output = process(dynamic.default, data);
})();
export { output };

导入 async 函数之后,在 then 方法里面再去使用我们导入的 output 变量,这样就确保了数据一定是动态加载之后的。

// usage.mjs
import promise, { output } from "./awaiting.mjs";
export function outputPlusValue(value) { return output + value }

promise.then(() => {
  console.log(outputPlusValue(100));
  setTimeout(() => console.log(outputPlusValue(100), 1000);
});
Top-level await

Top-level await 允许你将整个 JS 模块视为一个巨大的 async 函数,这样就可以直接在顶层使用 await,而不必用 async 函数包一层。
那么来重写上面的例子吧。

// awaiting.mjs
import { process } from "./some-module.mjs";
const dynamic = import(computedModuleSpecifier);
const data = fetch(url);
export const output = process((await dynamic).default, await data);

可以看到,直接在外层 使用 await 关键字来获取 dynamic 这个 Promise 的返回值,这种写法解决了原来因为 async 函数导致的各种问题。

你可能感兴趣的:(动态 import & top-level await)