webpack懶載入程式碼原理深究

一隻在學習的404發表於2020-11-12

背景介紹:

我們在實際的開發過程中,vue-router的元件經常這樣去寫:

{
  component: () => import('my/component/path/*.vue')
}

這樣寫的目的是實現懶載入,也就是當需要該元件的時候才去實際傳送請求。我們模擬懶載入,然後分析下構建後的原始碼

檔案準備(檔案連結)

a.js

  import('./b.js');

b.js

  console.log("hello");

webpack.config.js

const path = require('path');
module.exports = {
	entry: path.resolve(__dirname, 'a.js'),
	devtool: false,
	output: {
		path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
	},
	mode: 'none',
}

package.json

{
  "name": "wptest",
  "version": "1.0.0",
  "description": "",
  "main": "a.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12"
  }
}

執行構建命令npm run dev即可在dist檔案下檢視到構建後的程式碼

程式碼分析

我們從入口main.js程式碼中進行分析(下面這行程式碼可以暫時先不看,等到看完整個文章在回來看~)

/******/ (function(modules) { // webpackBootstrap
// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
	var chunkIds = data[0];
	var moreModules = data[1];
/******/
/******/
	// add "moreModules" to the modules object,
	// then flag all "chunkIds" as loaded and fire callback
	var moduleId, chunkId, i = 0, resolves = [];
	for(;i < chunkIds.length; i++) {
		chunkId = chunkIds[i];
		if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
			resolves.push(installedChunks[chunkId][0]);
		}
		installedChunks[chunkId] = 0;
	}
	for(moduleId in moreModules) {
		if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
			modules[moduleId] = moreModules[moduleId];
		}
	}
	if(parentJsonpFunction) parentJsonpFunction(data);
/******/
	while(resolves.length) {
		resolves.shift()();
	}
/******/
};
/******/
/******/
// The module cache
var installedModules = {};
/******/
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
	0: 0
};
/******/
/******/
/******/
// script path function
function jsonpScriptSrc(chunkId) {
	return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".js"
}
/******/
// The require function
function __webpack_require__(moduleId) {
/******/
	// Check if module is in cache
	if(installedModules[moduleId]) {
		return installedModules[moduleId].exports;
	}
	// Create a new module (and put it into the cache)
	var module = installedModules[moduleId] = {
		i: moduleId,
		l: false,
		exports: {}
	};
/******/
	// Execute the module function
	modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
	// Flag the module as loaded
	module.l = true;
/******/
	// Return the exports of the module
	return module.exports;
}
/******/
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
	var promises = [];
/******/
/******/
	// JSONP chunk loading for javascript
/******/
	var installedChunkData = installedChunks[chunkId];
	if(installedChunkData !== 0) { // 0 means "already installed".
/******/
		// a Promise means "currently loading".
		if(installedChunkData) {
			promises.push(installedChunkData[2]);
		} else {
			// setup Promise in chunk cache
			var promise = new Promise(function(resolve, reject) {
				installedChunkData = installedChunks[chunkId] = [resolve, reject];
			});
			promises.push(installedChunkData[2] = promise);
/******/
			// start chunk loading
			var script = document.createElement('script');
			var onScriptComplete;
/******/
			script.charset = 'utf-8';
			script.timeout = 120;
			if (__webpack_require__.nc) {
				script.setAttribute("nonce", __webpack_require__.nc);
			}
			script.src = jsonpScriptSrc(chunkId);
/******/
			// create error before stack unwound to get useful stacktrace later
			var error = new Error();
			onScriptComplete = function (event) {
				// avoid mem leaks in IE.
				script.onerror = script.onload = null;
				clearTimeout(timeout);
				var chunk = installedChunks[chunkId];
				if(chunk !== 0) {
					if(chunk) {
						var errorType = event && (event.type === 'load' ? 'missing' : event.type);
						var realSrc = event && event.target && event.target.src;
						error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
						error.name = 'ChunkLoadError';
						error.type = errorType;
						error.request = realSrc;
						chunk[1](error);
					}
					installedChunks[chunkId] = undefined;
				}
			};
			var timeout = setTimeout(function(){
				onScriptComplete({ type: 'timeout', target: script });
			}, 120000);
			script.onerror = script.onload = onScriptComplete;
			document.head.appendChild(script);
		}
	}
	return Promise.all(promises);
};
/******/
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
/******/
// expose the module cache
__webpack_require__.c = installedModules;
/******/
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
	if(!__webpack_require__.o(exports, name)) {
		Object.defineProperty(exports, name, { enumerable: true, get: getter });
	}
};
/******/
// define __esModule on exports
__webpack_require__.r = function(exports) {
	if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
		Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
	}
	Object.defineProperty(exports, '__esModule', { value: true });
};
/******/
// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
	if(mode & 1) value = __webpack_require__(value);
	if(mode & 8) return value;
	if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
	var ns = Object.create(null);
	__webpack_require__.r(ns);
	Object.defineProperty(ns, 'default', { enumerable: true, value: value });
	if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
	return ns;
};
/******/
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
	var getter = module && module.__esModule ?
		function getDefault() { return module['default']; } :
		function getModuleExports() { return module; };
	__webpack_require__.d(getter, 'a', getter);
	return getter;
};
/******/
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
// __webpack_public_path__
__webpack_require__.p = "";
/******/
// on error function for async loading
__webpack_require__.oe = function(err) { console.error(err); throw err; };
/******/
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
/******/
/******/
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

__webpack_require__.e(/* import() */ 1).then(__webpack_require__.t.bind(null, 1, 7));

/***/ })
/******/ ]);

可以看到206行,檔案執行了__webpack_require__.e()這個函式。這個函式後面跟的是一個then函式,可以推測出它返回的是一個promise。我們找到__webpack_require__.e這個函式,在75行。

這裡為了節省翻程式碼的時間,我直接將程式碼展示在這裡,並且所有的內容解釋都新增註釋在這裡

__webpack_require__.e = function requireEnsure(chunkId) {
  // 對應結果返回的Promise.all();
  // 如果已載入的話,那麼return返回的就直接是Promise.resolve([]);
	var promises = [];
  // installedChunks是一個快取chunk的機制,根據它的註釋我們可以看出來它一共有幾種狀態,chunkId就是我們非同步載入的模組的id
  var installedChunkData = installedChunks[chunkId]; 
  // 1、 0 // chunk已載入
  // 2、[resolve, reject, promise] // 陣列,代表正在載入中,載入的promise是陣列下標2,第0和1個元素分別是這個promise的resolve和reject
	if(installedChunkData !== 0) { // 0 means "already installed".
/******/
    // a Promise means "currently loading".
    // 有值但不是0,代表這個模組已經被載入過了但是處於loading態,所以直接返回之前的promise即可
		if(installedChunkData) {
			promises.push(installedChunkData[2]);
		} else {
      // 沒有值,那麼我們新建立一個promise,代表請求中...
			var promise = new Promise(function(resolve, reject) {
				installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      // 實際返回的promise
			promises.push(installedChunkData[2] = promise);


      // 開啟的實際請求!!!,建立script標籤!
			var script = document.createElement('script');
      var onScriptComplete;
      
			script.charset = 'utf-8';
			script.timeout = 120;
			if (__webpack_require__.nc) {
				script.setAttribute("nonce", __webpack_require__.nc);
      }
      // 處理資源url
			script.src = jsonpScriptSrc(chunkId);


      var error = new Error();
      // onload或者onerror都執行這個函式
			onScriptComplete = function (event) {
        // avoid mem leaks in IE.
        // 清空
        script.onerror = script.onload = null;
        // 取消計時器
        clearTimeout(timeout);
        // 獲取chunk
        var chunk = installedChunks[chunkId];
        // 如果chunk已經成功載入的話,那麼chunk肯定是0(這個地方後續會在講解為什麼)
        // 如果chunk載入失敗的話,那麼它還是那個陣列,也就是會執行下面if裡面的條件了
				if(chunk !== 0) {
					if(chunk) {
						var errorType = event && (event.type === 'load' ? 'missing' : event.type);
						var realSrc = event && event.target && event.target.src;
						error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
						error.name = 'ChunkLoadError';
						error.type = errorType;
						error.request = realSrc;
						chunk[1](error);
					}
					installedChunks[chunkId] = undefined;
				}
      };
      // 超時,也是一種error,
			var timeout = setTimeout(function(){
				onScriptComplete({ type: 'timeout', target: script });
      }, 120000);
      // 這裡面error和onload都呼叫onScriptComplete
      script.onerror = script.onload = onScriptComplete;
      // 插到頭部,開始做實際的js請求了...
			document.head.appendChild(script);
		}
  }
  // 返回這個promise
	return Promise.all(promises);
};

看完了上方的程式碼肯定會有如下幾個疑問

  • 返回的promise什麼時候被resolve了?沒有看到被resolve的時候了啊?
  • 為什麼載入成功了installedChunks[chunkId]就變成了0啊?

我們先明確如下一個概念:

<script src="src" onload="loadJs()"></script>

當script標籤載入成功的時候,會先執行script標籤載入到的內容,然後才回去執行onload函式。

明確了上面的概念我們看下打包後的b.js(1.js),也就是載入到了什麼內容

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],[
/* 0 */,
/* 1 */
/***/ (function(module, exports) {

console.log("hello world");

/***/ })
]]);

平平無奇的程式碼中透漏著絲絲詭異。window["webpackJsonp"]是啥?執行push操作push了個啥?我們找到這塊的相關程式碼,在main.js中的第190行到193行

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback; // push的程式碼在這裡
jsonpArray = jsonpArray.slice();

我們在找下webpackJsonpCallback這個函式(第三行)
同理,我們所有的說明都放在註釋裡面

function webpackJsonpCallback(data) {
	var chunkIds = data[0]; // 配合b.js和變數名可以輕而易舉的得知這是chunkId的陣列
	var moreModules = data[1]; // module的實際內容
/******/
/******/
	// add "moreModules" to the modules object,
	// then flag all "chunkIds" as loaded and fire callback
  var moduleId, chunkId, i = 0, resolves = [];
  // 迴圈chunkId
	for(;i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    // 如果已經快取了,那麼就把它的第0個元素放到resolves這個陣列裡面
    // 第0個元素是什麼嘛?([resolve, reject, promise])
		if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
			resolves.push(installedChunks[chunkId][0]);
    }
    // 在這裡寫成0了。為什麼執行到這裡代表載入成功?別忘記我們這個函式是從哪裡呼叫的~
		installedChunks[chunkId] = 0;
  }
  // module的呼叫
	for(moduleId in moreModules) {
		if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
			modules[moduleId] = moreModules[moduleId];
		}
	}
	if(parentJsonpFunction) parentJsonpFunction(data);
/******/
  // 迴圈呼叫resolves裡面的resolve,至此installedChunks[chunkId]變成了0,之前處於loading的promise被resolve了
	while(resolves.length) {
		resolves.shift()();
	}
/******/
};

至此可以明白所有的非同步載入都已經載入完畢了。

其實懶載入的本質就是到需要的時候在去建立script標籤去載入對應的資源。可能當我們沒理解的時候會好奇為什麼,但是當我們扒開它神祕的面紗,它其實“一覽無遺”

我是殘心,感謝你的每一個點贊關注

相關文章