webpack懶載入程式碼原理深究
背景介紹:
我們在實際的開發過程中,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標籤去載入對應的資源。可能當我們沒理解的時候會好奇為什麼,但是當我們扒開它神祕的面紗,它其實“一覽無遺”
我是殘心,感謝你的每一個點贊關注
相關文章
- 前端開發-- Webpack 程式碼分割和懶載入技術前端Web
- 關於懶載入原理
- 圖片懶載入原理
- webpack4 系列教程(四): 單頁面解決方案–程式碼分割和懶載入Web
- webpack 非同步載入原理Web非同步
- 滾動載入圖片(懶載入)實現原理
- 程式碼分割與懶載入情況下(code-splitting+lazyload)抽離懶載入模組的公用模組程式碼
- 30行Javascript程式碼實現圖片懶載入JavaScript
- 懶載入
- 深入理解React:懶載入(lazy)實現原理React
- 懶載入和預載入
- webpack,非同步載入,程式碼分割,require.ensureWeb非同步UI
- Ribbon - 懶載入
- Spring5.0原始碼學習系列之淺談懶載入機制原理Spring原始碼
- .NET 透過原始碼深究依賴注入原理原始碼依賴注入
- webpack模組非同步載入原理解析Web非同步
- 【譯】懶載入元件元件
- 圖片懶載入
- vue路由懶載入Vue路由
- Vue元件懶載入Vue元件
- Hibernate 之 懶載入
- Vue 的懶載入Vue
- 圖片預載入和懶載入
- Vue 路由按需載入(路由懶載入)Vue路由
- 前端效能優化 --- 懶載入&預載入前端優化
- 圖片懶載入(IntersectionObserver)Server
- Fragment 懶載入實踐Fragment
- 懶載入之intersection observerServer
- vue(18)路由懶載入Vue路由
- 手把手實現圖片懶載入+封裝vue懶載入元件封裝Vue元件
- 為什麼我寫了路由懶載入但程式碼卻沒有分割?路由
- 小說APP原始碼的圖片載入方式,懶載入和預載入的實現APP原始碼
- [譯] React 16.6 懶載入(與預載入)元件React元件
- 小程式記憶體問題–圖片懶載入記憶體
- 微信小程式--實現圖片懶載入(lazyload)微信小程式
- 藉助 Webpack 靜態分析能力實現程式碼動態載入Web
- 圖片懶載入踩坑
- 圖片懶載入大白話