前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一起来看 Dynamic Import 和 Top-level await

一起来看 Dynamic Import 和 Top-level await

作者头像
尹光耀
发布2021-10-19 10:14:26
8570
发布2021-10-19 10:14:26
举报
文章被收录于专栏:前端小馆前端小馆

1. 前言

随着 ES6 的发布,JavaScript 语法也越来越趋于成熟,新的提案也在不断地提出。

ECMA 提案一共有四个阶段,处于 Stage3 的都需要我们持续关注,以后很可能就会被纳入新标准中。

今天主要来深入讲解一下动态 import 和 Top-level await。

1. Dynamic Import

如果你写过 Node,会发现和原生的 import/export 有个不一样的地方就是 Node 支持就近加载。

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

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

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

代码语言:javascript
复制
// noop.js
console.log('noop');
module.exports = function() {}
// main.js
console.log('main')
const noop = require('./noop')

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

代码语言:javascript
复制
// noop.js
console.log('noop');
export default function() {}
// main.js
console.log('main')
import noop from './noop'

在我们前端开发中,为了优化用户体验,往往需要对页面资源按需加载。

如果只想在用户进入某个页面的时候再去加载这个页面的资源,那么就可以配合路由去动态加载资源。

1.1 React Suspense

在很久很久之前,我们都是用 webpack 提供的 require.ensure() 来实现 React 路由切割。

代码语言:javascript
复制
const rootRoute = {
  path: '/',
  indexRoute: {
    getComponent(nextState, cb) {
      require.ensure([], (require) => {
        cb(null, require('pages/Home'))
      }, 'Home')
    },
  },
  getComponent(nextState, cb) {
    require.ensure([], (require) => {
      cb(null, require('pages/Login'))
    }, 'Login')
  }
}

ReactDOM.render(
  (
    <Router
      history={browserHistory}
      routes={rootRoute}
      />
  ), document.getElementById('app')
);

在 React16 中,已经提供了 Suspense/lazy 支持了按需加载。我们可以通过 Dynamic Import 来加载页面,配合 Suspense 实现路由分割。

代码语言:javascript
复制
import react, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./pages/home'))
const Login = lazy(() => import('./pages/login'))
function Routes() { 
    return (
        <Router>
            <Suspense fallback={<div>loading</div>}>
                <Switch>
                    <Route exact path="/" component={Home} />
                     <Route path="/login" component={Login} />
                </Switch>
            </Suspense>
        </Router>
    )
}

1.2 动态 import 提案

由于各种历史原因,一个动态 import 的提案就被提了出来,这个提案目前已经走到了 Stage4 阶段。

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

代码语言:javascript
复制
const router = new Router({
    routes: [{
        path: '/home',
        name: 'Home',
        component: () =>
            import('./pages/Home.vue')
    }]
})

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

代码语言:javascript
复制
<nav>
  <a href="books.html" data-entry-module="books">Books</a>
  <a href="movies.html" data-entry-module="movies">Movies</a>
  <a href="video-games.html" data-entry-module="video-games">Video Games</a>
</nav>

<main>Content will load here!</main>

<script>
  const main = document.querySelector("main");
  for (const link of document.querySelectorAll("nav > a")) {
    link.addEventListener("click", e => {
      e.preventDefault();

      import(`./section-modules/${link.dataset.entryModule}.js`)
        .then(module => {
          module.loadPageInto(main);
        })
        .catch(err => {
          main.textContent = err.message;
        });
    });
  }
</script>

1.3 手动实现一个动态 import 函数

其实我们自己也完全可以通过 Promise 来封装这样一个 api,核心在于动态生成 script 标签,在 script 中导入需要懒加载的模块,将其挂载到 window 上面。

代码语言:javascript
复制
function importModule(url) {
    return new Promise((resolve, reject) => {
        const script = document.createElement("script");
        script.type = "module";
        script.textContent = `import * as m from "${url}"; window.tempModule = m;`;
    })
}

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

代码语言:javascript
复制
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);
  });
}

这个 importModule 也是官方推荐的在不支持动态 import 的浏览器环境中的一种实现。

2. Top-level await

前面讲了动态 import,但是如果想在动态引入某个模块之后再导出当前模块的数据,那么该怎么办呢?

如果在模块中我依赖了某个需要异步获取的数据之后再导出数据怎么办?

2.1 ES Module 的缺陷

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

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

举个栗子:

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

代码语言:javascript
复制
// 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 导出的是快照。

2.2 IIAFEs 的局限性

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

代码语言:javascript
复制
// 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。

代码语言:javascript
复制
// 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,例如:

代码语言:javascript
复制
import { output } from './awaiting'
setTimeout(() => {
    console.log(output)
}, 2000)

2.3 升级版的 IIAFEs

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

代码语言:javascript
复制
// 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 变量,这样就确保了数据一定是动态加载之后的。

代码语言:javascript
复制
// 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);
});

2.4 Top-level await

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

代码语言:javascript
复制
// 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 函数导致的各种问题。

Top-level await 现在处于 Stage3 阶段。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 前言
  • 1. Dynamic Import
    • 1.1 React Suspense
      • 1.2 动态 import 提案
        • 1.3 手动实现一个动态 import 函数
        • 2. Top-level await
          • 2.1 ES Module 的缺陷
            • 2.2 IIAFEs 的局限性
              • 2.3 升级版的 IIAFEs
                • 2.4 Top-level await
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档