簡單易懂的 webpack 打包後 JS 的運行過程(二)

hello~親愛的看官老爺們你們好~上週寫下一篇 簡單易懂的 webpack 打包後 JS 的運行過程 後,仍是挺受小夥伴們歡迎的。然而這篇文章挖了坑還沒填完,此次就把剩下的內容補完。css

本文主要是關於異步加載的 js 是如何執行,較少使用 webpack 問題也不大。而若是看過前一篇文章相關的知識那就更好了。若已經瞭解過相關知識的小夥伴,不妨快速閱讀一下,算是溫故知新,實際上是想請你告訴我哪裏寫得不對html

簡單配置

webpack 的配置就不貼出來了,就是肯定一下入口,提取 webpack 運行時須要用到的 minifest.js 而已。這裏簡單貼一下 html 模板與須要的兩個 js 文件:webpack

<!--index.html-->
<!doctype html>
<html lang="en">
<body>
    <p class="p">Nothing yet.</p>
    <button class="btn">click</button>
</body>
</html>


//index.js
const p = document.querySelector('.p');
const btn = document.querySelector('.btn');
btn.addEventListener('click', function() {
  //只有觸發事件纔回家再對應的js 也就是異步加載 
  require.ensure([], function() {
    const data = require('./src/js/test');
    p.innerHTML = data;
  })
})

//test.js
const data = 'success!';
module.exports = data;
複製代碼

這樣配置示例配置就完成了。可能有小夥伴不太熟悉 require.ensure,簡單地說,就是告訴 webpack,請懶加載 test.js,別一打開頁面就給我下載下來。相關的知識不妨看這裏git

打包完的目錄架構畫風是這樣的:github

至此,配置就完成啦~web

index.js 開始探索

先用瀏覽器打開 index.html,查看資源加載狀況,能發現只加載了 index.jsminifest.js數組

以後點擊按鈕,會再加多一個 0.7f0a.jspromise

能夠說明代碼是被分割了的,只要當對應的條件觸發時,瀏覽器纔會去加載指定的資源。而不管以後咱們點擊多少次,0.7f0a.js 文件都不會重複加載,此時小本本應記下第一個問題:如何作到不重複加載。瀏覽器

按照加載順序,實際上是應該先看 minifest.js 的,但不妨先看看 index.js 的代碼,帶着問題有助於尋找答案。代碼以下:網絡

webpackJsonp([1], {
  "JkW7":
    (function(module, exports, __webpack_require__) {
      const p = document.querySelector('.p');
      const btn = document.querySelector('.btn');

      btn.addEventListener('click', function() {
        __webpack_require__.e(0).then((function() {
          const data = __webpack_require__("zFrx");
          p.innerHTML = data;
        }).bind(null, __webpack_require__)).catch(__webpack_require__.oe)
      })
    })
}, ["JkW7"]);
複製代碼

可能有些小夥伴已經忘記了上一篇文章的內容,__webpack_require__ 做用是加載對應 module 的內容。這裏提一句, module 其實就是打包前,import 或者 require 的一個個 js 文件,如test.jsindex.js。後文說到的 chunk 是打包後的文件,即 index.ad23.jsmanifest.473d.js0.7f0a.js文件。一個 chunk 可能包含若干 module

回憶起相關知識後,咱們看看異步加載到底有什麼不一樣。index.js 中最引入注目的應該是 __webpack_require__.e 這個方法了,傳入一個數值以後返回一個 promise。這方法當 promise 決議成功後執行切換文本的邏輯,失敗則執行 __webpack_require__.oe。於是小本本整理一下,算上剛纔的問題,須要爲這些問題找到答案:

  • 如何作到不重複加載。
  • __webpack_require__.e 方法的邏輯。
  • __webpack_require__.oe 方法的邏輯。

minifest.js 中尋找答案

咱們先查看一下 __webpack_require__.e 方法,爲方法查看起見,貼一下對應的代碼,你們不妨先試着本身尋找一下剛纔問題的答案。

var installedChunks = {
  2: 0
};

__webpack_require__.e = function requireEnsure(chunkId) {
  var installedChunkData = installedChunks[chunkId];
  if (installedChunkData === 0) {
    return new Promise(function(resolve) {
      resolve();
    });

  }
  if (installedChunkData) {
    return installedChunkData[2];
  }

  var promise = new Promise(function(resolve, reject) {
    installedChunkData = installedChunks[chunkId] = [resolve, reject];
  });
  installedChunkData[2] = promise;
  var head = document.getElementsByTagName('head')[0];
  var script = document.createElement('script');
  script.src = "js/" + chunkId + "." + {
    "0": "7f0a",
    "1": "ad23"
  }[chunkId] + ".js";
  script.onerror = script.onload = onScriptComplete;

  function onScriptComplete() {
    script.onerror = script.onload = null;
    var chunk = installedChunks[chunkId];
    if (chunk !== 0) {
      if (chunk) {
        chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
      }
      installedChunks[chunkId] = undefined;
    }
  };
  head.appendChild(script);
  return promise;
};
複製代碼

該方法中接受一個名爲 chunkId 的參數,返回一個 promise,印證了咱們閱讀 index.js 時的猜測,也確認了傳入的數字是 chunkId。以後變量 installedChunkData 被賦值爲對象 installedChunks 中鍵爲 chunkId 的值,能夠推想出 installedChunks 對象其實就是記錄已加載 chunk 的地方。此時咱們還沒有加載對應模塊,理所固然是 undefined

以後咱們想跳過兩個判斷,查看一下 __webpack_require__.e 方法返回值的 promise 是怎樣的:

var promise = new Promise(function(resolve, reject) {
    installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
複製代碼

能夠看到 installedChunkDatainstalledChunks[chunkId] 被從新賦值爲一個數組,存放着返回值 promiseresolvereject,而使人不解的是,爲什麼將數組的第三項賦值爲這個 promise呢?

其實此前有一個條件判斷:

if (installedChunkData) {
    return installedChunkData[2];
}
複製代碼

那你明白爲何了嗎?在此例中1,假設網絡不好的狀況下,咱們瘋狂點擊按鈕,爲避免瀏覽器發出若干個請求,經過條件判斷都返回同一個 promise,當它決議後,全部掛載在它之上的 then 方法都能獲得結果運行下去,至關於構造了一個隊列,返回結果後按順序執行對應方法,此處仍是十分巧妙的。

以後就是創造一個 script 標籤插入頭部,加載指定的 js 了。值得關注的是 onScriptComplete 方法中的判斷:

var chunk = installedChunks[chunkId];
if (chunk !== 0) {
    ...
}
複製代碼

明明 installedChunks[chunkId] 被賦值爲數組,它確定不可能爲0啊,這不是鐵定失敗了麼?先別急,要知道 js 文件下載成功以後,先執行內容,再執行 onload 方法的,那麼它的內容是什麼呢?

webpackJsonp([0], {
  "zFrx":
    (function(module, exports) {
      const data = 'success!';
      module.exports = data;
    })
});
複製代碼

能夠看到,和 index.js 仍是很像的。這個 js 文件的 chunkId 是0。它的內容很簡單,只不過是 module.exports 出去了一些東西。關鍵仍是 webpackJsonp 方法,此處截取關鍵部分:

var resolves = [];

for (; i < chunkIds.length; i++) {
  chunkId = chunkIds[i];
  if (installedChunks[chunkId]) {
    resolves.push(installedChunks[chunkId][0]);
  }
  installedChunks[chunkId] = 0;
}

while (resolves.length) {
  resolves.shift()();
}
複製代碼

當它執行的時候,會判斷 installedChunks[chunkId] 是否存在,若存在則往數組中 push(installedChunks[chunkId][0]) 並將 installedChunks[chunkId] 賦值爲0; 。還得記得數組的首項是什麼嗎?是 __webpack_require__.e 返回 promiseresolve!以後執行這個 resolve。固然, webpackJsonp 方法會將下載下來文件全部的 module 存起來,當 __webpack_require__ 對應 modulIde 時,返回對應的值。

讓咱們目光返回 __webpack_require__.e 方法。 已知對應的 js 文件下載成功後,installedChunks[chunkId] 被賦值爲0。文件執行完或下載失敗後都會觸發 onScriptComplete 方法,在該方法中,如若 installedChunks[chunkId] !== 0,這是下載失敗的狀況,那麼此時 installedChunks[chunkId] 的第二項是返回 promisereject,執行這個 reject 以拋出錯誤:

if (chunk !== 0) {
  if (chunk) {
    chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
  }
  installedChunks[chunkId] = undefined;
}
複製代碼

當再次請求同一文件時,因爲對應的 module 已經被加載,於是直接返回一個成功的 promise 便可,對應的邏輯以下:

var installedChunkData = installedChunks[chunkId];
if (installedChunkData === 0) {
  return new Promise(function(resolve) {
    resolve();
  });
}
複製代碼

最後看一下 __webpack_require__.oe 方法:

__webpack_require__.oe = function(err) { console.error(err); throw err; };
複製代碼

特別簡單對吧?最後整理一下流程:當異步請求文件發起時,先判斷該 chunk 是否已被加載,是的話直接返回一個成功的 promise,讓 then 執行的函數 require 對應的 module 便可。否則則構造一個 script 標籤加載對應的 chunk,下載成功後掛載該 chunk 內全部的 module。下載失敗則打印錯誤。

小結

以上就是 webpack 異步加載 js 文件過程的簡單描述,其實流程真的特別簡單易懂,只是代碼的編寫十分巧妙,值得仔細研究學習。對應的代碼會放到 github 中,歡迎查閱點 star

感謝各位看官大人看到這裏,知易行難,但願本文對你有所幫助~謝謝!

相關文章
相關標籤/搜索