通过打包后的源码解析 Webpack 懒加载原理 🤓🤓🤓
面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:
yunmz777
。
Webpack 的懒加载通过动态 import()
语法实现按需加载模块。它将代码分割成多个独立的 chunk,只有在需要时才加载相应的模块,从而减少初始加载的代码量。通过 SplitChunksPlugin
,Webpack 还可以提取共享依赖,避免重复加载。开发者还可以使用 webpackChunkName
注释自定义 chunk 名称,进一步优化加载过程。
demo 演示
首先我们先来看一个简单的 demo 演示吧,如下目录结构所示:
接着我们有这样的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="button">点击按钮懒加载</button>
</body>
<script src="../dist/main.js"></script>
</html>
// a.js
export default () => {
console.log("Moment");
};
// main.js
const buttonEle = document.getElementById("button");
buttonEle.onclick = function () {
//懒加载 a 模块
import("./a").then((module) => {
const callback = module.default;
callback();
});
};
除了这些基础的配置之外,我们还有一些 webpack 配置和相关的依赖:
module.exports = {
mode: "development",
entry: "./src/main.js",
};
还有相关的 package.json 信息:
{
"name": "lazy",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --config ./webpack.config.js --env production"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"webpack": "^5.99.5"
},
"devDependencies": {
"webpack-cli": "^6.0.1"
}
}
我们执行 pnpm build
之后会得到如下文件:
其中,src_a_js.js
就是通过 import("./a")
懒加载的模块:
我们在浏览器中打开 index.html,打开控制台:
首次加载只能看到只加载了 main.js 这个文件,而且浏览器上是没有任何的输出。
当我们点击按钮之后,才会执行后,才会执行 src_a_js.js
模块,控制台打印 Moment
原理解析
首先我们先来讲解一下大概的执行流程:
-
点击按钮后,加载模块:通过
jsonp
异步加载a.js
模块对应的文件。 -
执行并合并模块:加载回来的
a.js
文件会在浏览器中执行,并将模块定义合并到main.js
的__webpack_modules__
中。 -
加载并缓存模块:模块加载后会被缓存,以便后续使用时直接返回缓存的内容。
-
导出并使用模块内容:获取并使用该模块导出的功能或对象。
1. __webpack_require__.e
__webpack_require__.e(chunkId)
是 Webpack 用于异步加载模块的关键方法。它返回一个 Promise
,并触发指定 chunkId
对应的模块(chunk)加载。当多个异步模块需要加载时,Webpack 会利用 Promise.all
确保在所有异步模块加载完毕之后,才执行后续的操作。调用 __webpack_require__.e("src_a_js")
时,Webpack 会根据 chunkId
加载对应的代码文件,确保所有必要的模块都已加载完成。
chunkId 一般为打包后的文件名:
如下代码所示:
__webpack_require__.e("src_a_js").then(() => {
// 所有异步模块加载完毕后执行的代码
});
2. __webpack_require__.f.j
__webpack_require__.f.j
是 Webpack 用来处理异步模块加载的核心函数。它会将异步模块的 Promise
添加到一个队列(promises
)中,并通过 installedChunks
跟踪每个模块的加载状态,以确保同一模块不会被重复加载。Webpack 维护一个 installedChunks
的 Map,记录每个 chunkId
的加载进度(是否已加载)。
每次加载异步模块时,__webpack_require__.f.j
会检查模块的加载状态,如果模块仍在加载中,它会将新的 Promise
加入 promises
队列,直到模块加载完成。这样,Webpack 能高效地管理模块加载,避免不必要的重复加载。
如下代码所示:
__webpack_require__.f.j = (chunkId, promises) => {
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) {
if (installedChunkData) {
promises.push(installedChunkData[2]); // 推入正在加载的 Promise
} else {
// 开始加载模块并更新 installedChunks
var promise = new Promise((resolve, reject) => {
installedChunks[chunkId] = [resolve, reject];
});
promises.push((installedChunkData[2] = promise));
// 触发实际的异步加载过程
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
__webpack_require__.l(url, loadingEnded, chunkId);
}
}
};
3. __webpack_require__.l
__webpack_require__.l
是 Webpack 用来异步加载模块的核心函数。它会动态创建一个 <script>
标签,向浏览器加载对应的异步模块文件(如 chunkId.js
)。
通过插入 <script>
标签,Webpack 向服务器请求所需的模块文件。当模块加载完成后,WebPack 会执行一个 IIFE(立即调用函数表达式),该函数将加载的模块内容添加到 Webpack 的模块缓存中,从而完成模块的加载和执行。
这种机制依赖于 JSONP 技术,通过动态加载 JavaScript 文件实现模块的按需加载。
如下代码所示:
__webpack_require__.l(url, done, chunkId);
4. IIFE 执行和 webpackChunk
的回调
当异步模块文件(如 src_a_js.js
)加载完成后,浏览器会执行返回的 IIFE 函数。这个 IIFE 会触发 webpackChunkwebpack.push
,通过 Webpack 维护的 webpackJsonpCallback
函数,将异步模块的内容合并到主模块的 __webpack_modules__
中。
具体来说,加载完成的异步模块文件会将 chunkId
、模块内容及运行时代码推送到 webpackChunkwebpack.push
中。随后,webpackJsonpCallback
函数会将这些模块内容添加到 Webpack 的 __webpack_modules__
缓存中,使得后续代码能够通过 __webpack_require__
正常访问和执行该模块。
这样,Webpack 确保了按需加载的模块能够正确集成进应用的模块系统中,支持异步加载和模块共享。
如下代码所示:
(self["webpackChunkwebpack"] = self["webpackChunkwebpack"] || []).push([
["src_a_js"], // chunkId
{
"./src/a.js": function (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__,
});
const __WEBPACK_DEFAULT_EXPORT__ = () => {
console.log("Moment");
};
},
},
]);
5. webpackJsonpCallback
函数
webpackJsonpCallback
函数充当 Webpack 主模块与异步模块之间的桥梁。它将异步加载的模块内容合并到主模块的 __webpack_modules__
中,从而实现模块的同步访问。
具体来说,webpackJsonpCallback
会根据 chunkId
在 installedChunks
中找到对应的 Promise
,并调用 resolve
方法。一旦 resolve
被触发,异步模块的代码就会执行,之后 __webpack_require__
可以同步地访问该模块的内容。
这一过程确保了异步模块能够在加载完成后与主模块的模块系统正确集成,支持高效的按需加载。
如下代码所示:
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
var [chunkIds, moreModules, runtime] = data;
for (var moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
if (runtime) runtime(__webpack_require__);
for (let i = 0; i < chunkIds.length; i++) {
installedChunks[chunkIds[i]] = 0;
}
parentChunkLoadingFunction && parentChunkLoadingFunction(data);
};
6. 完成加载:执行 __webpack_require__(chunkId)
当所有 Promise
完成时,__webpack_require__.e
返回的 Promise.all
会进入 then
方法,此时会调用 __webpack_require__(chunkId)
来执行并返回异步模块的导出内容(module.exports
)。
Promise.all
会等待所有异步模块加载完成后,才会执行后续的代码。此时,__webpack_require__(chunkId)
会像同步模块一样执行异步模块的代码,并返回其 module.exports
,确保模块在加载完成后能够正常访问。
如下代码所示:
__webpack_require__(chunkId).then((module) => {
// 执行异步模块的代码,返回 module.exports
});
总结
Webpack 的懒加载通过按需加载模块来优化性能。当某个模块被异步加载时,Webpack 会将其拆分为单独的 chunk,只有在需要时才会加载这些模块。使用 __webpack_require__.e
方法触发模块加载,并通过 Promise
确保模块加载完成后再执行后续操作。模块加载完成后,Webpack 会将其缓存,以便后续访问时直接使用缓存的内容,从而减少不必要的加载和提高页面加载速度。
它的加载原理如下步骤所示:
-
__webpack_require__.e
返回一个Promise
,确保异步模块加载完成。 -
__webpack_require__.f.j
将异步模块的Promise
添加到加载队列。 -
__webpack_require__.l
创建<script>
标签,加载异步模块。 -
异步模块加载完成,执行 IIFE,调用
webpackJsonpCallback
。 -
webpackJsonpCallback
合并异步模块到主模块的__webpack_modules__
中。 -
异步模块的
Promise
被解析,执行__webpack_require__(chunkId)
,返回模块内容。
通过这种方式,Webpack 实现了异步模块加载、按需加载和模块缓存,使得浏览器能够高效地加载和执行模块代码。