深刻理解 动态 Import 和 顶层 await

1. 前言

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

ECMA 提案一共有四个阶段,处于 Stage3 的都须要咱们持续关注,之后极可能就会被归入新标准中。前端

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

动态import

1. Dynamic Import

若是你写过 Node,会发现和原生的 import/export 有个不同的地方就是 CommonJS 支持就近加载。react

CommonJS 容许你能够在用到的时候再去加载这个模块,而不用所有放到顶部加载。webpack

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

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

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

若是换成 import/export,无论你将 import 放到哪里,打印结果都是相反的。好比下面依次打印的是 noopmainpromise

// 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 路由切割。

const rootRoute = {
  path'/',
  indexRoute: {
    getComponent(nextState, cb) {
      require.ensure([], (require) => {
        cb(nullrequire('pages/Home'))
      }, 'Home')
    },
  },
  getComponent(nextState, cb) {
    require.ensure([], (require) => {
      cb(nullrequire('pages/Login'))
    }, 'Login')
  }
}

ReactDOM.render(
  (
    <Router
      history={browserHistory}
      routes={rootRoute}
      />

  ), document.getElementById('app')
);

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

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 模块,而不会在最开始的时候就将所有模块加载。

const router = new Router({
    routes: [{
        path: '/home',
        name: 'Home',
        component: () =>
            import('./pages/Home.vue')
    }]
})

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

<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 标签。
首先咱们返回一个新的 Promise,而后建立一个 script 元素。

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

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

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);
  });
}

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

2. Top-level await

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

若是在模块中我依赖了某个须要异步获取的数据以后再导出数据怎么办?

2.1 ES Module 的缺陷

若是你认真研究过 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 导出的是快照。

2.2 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)

2.3 升级版的 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(valuereturn 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 函数包一层。
那么来重写上面的例子吧。

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

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

Top-level await 如今处于 Stage3 阶段。


若是你喜欢探讨技术,或者对本文有任何的意见或建议,你能够扫描下方二维码,关注微信公众号“ 鱼头的Web海洋 ”,随时与鱼头互动。欢迎!衷心但愿能够碰见你。

本文分享自微信公众号 - 鱼头的Web海洋(krissarea)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索