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

sea_ljf發表於2017-12-16

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

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

簡單配置

webpack 的配置就不貼出來了,就是確定一下入口,提取 webpack 執行時需要用到的 minifest.js 而已。這裡簡單貼一下 html 模板與需要的兩個 js 檔案:

<!--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,別一開啟頁面就給我下載下來。相關的知識不妨看這裡

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

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

至此,配置就完成啦~

index.js 開始探索

先用瀏覽器開啟 index.html,檢視資源載入情況,能發現只載入了 index.jsminifest.js

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

之後點選按鈕,會再加多一個 0.7f0a.js

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

可以說明程式碼是被分割了的,只要當對應的條件觸發時,瀏覽器才會去載入指定的資源。而無論之後我們點選多少次,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

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

相關文章