深刻理解 動態 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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索