webpack 懒加载(异步加载)原理

您所在的位置:网站首页 webpack基础配置插件 webpack 懒加载(异步加载)原理

webpack 懒加载(异步加载)原理

2023-05-31 09:25| 来源: 网络整理| 查看: 265

在 JS 模块中,我们可以通过 import() 语法动态导入某个模块,也就是懒加载模块,那么 webpack 是如何处理懒加载的呢?

demo 演示

我们先来写一个 demo

// .src/a.js export default () => { console.log("Jolyne"); }; // .src/main.js const buttonEle = document.getElementById("button"); buttonEle.onclick = function () { //懒加载 a 模块 import("./a").then((module) => { const callback = module.default; callback(); }); }; // .src/index.html DOCTYPE html> Document 点击按钮懒加载

webpack 配置:

// webpack.config.js module.exports = { mode: "development", entry: "./src/main.js", devtool: "source-map", }

package.json 脚本

// package.json { "scripts": { "build": "webpack --config ./webpack.config.js --env production", }, }

我们执行 npm run build 后,得到如下的文件:

image.png

其中,src_a_js.js 就是通过 import("./a") 懒加载的模块

我们在浏览器中打开 index.html,打开控制台:

image.png

此时只加载了 main.js,且控制台没有任何输出。当我点击按钮后,才会执行 src_a_js.js 模块,控制台打印 Jolyne

image.png

image.png

原理解析

我们打包代码后,在 ./dist/main.js 里面能找到如下的代码:

image.png

我们发现他会分为三个步骤:

调用 __webpack_require_.e("src_a_js"),返回 Promise 调用 webpack\_require.bind(webpack\_require, "./src/a.js") 通过 module.default 拿到 () => { console.log("Jolyne") } 并执行该 default 函数

我们来拆解他们分别做了什么

_webpack_require.e

这个函数做的事情可以概括为:

第一部分:通过 Jsonp 的形式去加载 懒加载的模块 第二部分:执行该模块,将该模块的代码合并到同步模块中

我们来看他是如何通过 JSONP 加载异步模块的:

//这个变量代表已经加载完毕的模块 var installChunks = { main: 0 // ./dist/main.js 这个模块已经加载完毕了 } //这个变量存储:所有同步模块,demo里面没有同步模块,所以当前是个空对象 var modules = {} const __webpack_require_.e = (chunkId) => { /** 第一部分:JSONP 加载模块 **/ const promises = [] const currentPromise = new Promise((resolve, reject) => { installChunks[chunkId] = [resove, reject] }) promises.push(currentPromise) const url = require.publicPath + chunkId //这个 url 就是拼接了 ./dist/src_a_js.js 的路径 const srcipt = document.createElement("script") srcipt.src = url document.head.appendChild(script) /** 第二部分:执行模块,合并到同步模块中 **/ //这个函数我们分析第二步的时候再细说 webpackJsonpCallback() return Promise.all(promises) }

首先,第一部分的代码的逻辑是:

创建一个 promises 数组 创建一个 promise 对象,以 chunkId(当前要去加载的这个异步模块的Id)为 key,将 promise 对象 的 resolve、reject 回调记录在 installChunks[chunkId] 上,此时: installChunks = { main: 0, src_a_js: [resolve, reject] } 拼接 url,创建 script 标签,设置 srcipt.src = url,然后加载 url 指向的模块

至于为什么要创建一个 Promise 对象,又要记录它的 resolve、reject 回调,我们在 第二部分 里面分析

第一部分加载回来的模块长这样:

(self["webpackChunkwebpack_module"] = self["webpackChunkwebpack_module"] || []).push([ ["src_a_js"], { "./src/a.js": ( __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("Jolyne"); }; }, }, ]);

然后需要执行该模块,那么如何执行呢?此时就会调用一个叫做 webpackJsonpCallback 的函数

//...第一部分的代码 /** chunkIds 就是上面 ["src_a_js"] moreModules 就是上面的: { "./src/a.js": ( __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("Jolyne"); }; }, }, **/ const webpackJsonpCallback = (chunkIds, moreModules) => { const resolves = [] for (moduleId in moreModules) { // modules 是第一部分里面存储所有同步模块的那个变量 // 相当于把异步模块的代码合并到 modules 上 modules[moduleId] = moreModules[moduleId] } for (let i = 0; i < chunkIds.length; i++) { // 还记得之前我们将 Promise 对象的 resolve、reject 回调记录在 installChunks 上吗 // 这里我们就是去取出 resolve 回调,放到 resolves 数组中去 resolves.push(installChunks[chunkIds[i]][0]) // 标识当前这个懒加载的模块已经执行完毕了 installChunks[chunkIds[i]] = 0 } while (resolves.length) { // 第一部分我们不是 return Promise.all(promises) 吗? // 这里相当于去执行 promises 里面所有的 resolve 回调 // 当 所有的 resolve 回调执行完毕之后,表明所有的懒加载模块都加载且合并完毕了 resolves.shift()() } // 执行到这里,__webpack_require_.e 这个函数才执行完毕 }

上面的代码的思路为:

创建 resolves 数组,用来存放第一部分我们创建的 Promise 对象的被记录的 resolve 回调 将懒加载的模块代码 合并到 modules 这个变量上 标识 installChunks[moduleId] = 0,表明懒加载的模块已经合并完成了 执行 resolves 数组里面所有的 resolve 回调,这样第一部分中 return Promise.all(promises) 才会继续下一步

此时 modules 变量为:

const modules = { "./src/a.js": (modules, exports, require) => { require.defineProperty(exports, { default: () => WEBPACK_DEFAULT_EXPORT, }); const WEBPACK_DEFAULT_EXPORT = () => { console.log("按钮点击了"); }; }, };

看完第二部分的操作后,我们来回答一下第一部分我们抛出的问题: 为什么要创建一个 Promise 对象,又要记录它的 resolve、reject 回调

创建 Promise 对象是为了能跟踪懒加载模块的状态(是否正在加载?是否加载完毕?),记录 resolve、reject 回调 是为了确保懒加载模块加载完毕且已经合并了代码,此时执行 resolve() 改变 Promise 的状态,第一部分的 return Promise.all(promises) 成功后,才继续下一步操作

至此,__webpack_require_e 函数执行完毕

webpack_require.bind(webpack_require, "./src/a.js")

这一步其实就是对 exports 对象做代理,然后 return exports 对象

const webpack_cache = {}; function require(moduleId) { var cache = webpack_cache[moduleId]; if (cache !== undefined) { return cache.exports; } var module = (webpack_cache[moduleId] = { exports: {}, }); // 对 exports 对象做代理 modules[moduleId](module, module.exports, require); // 返回 exports 对象 return module.exports; } require.defineProperty = (exports, definition) => { for (var key in definition) { Object.defineProperty(exports, key, { enumerable: true, get: definition[key], }); } };

这一部分可以去看 # webpack模块化原理解析,这里不细说了

然后抛出的 exports 对象就可以在第三步拿到了,也就是:

const buttonEle = document.getElementById("button"); buttonEle.onclick = function () { //第一步 __webpack_require__.e(/*! import() */ "src_a_js") .then( __webpack_require__.bind(__webpack_require__, /*! ./a */ "./src/a.js") ) .then((module) => { //在这里,就能拿到 exports 对象了 const callback = module.default; callback(); // Jolyne }); }; 总结

webpack 模块懒加载的原理:

创建 Promise 对象(用来跟踪懒加载模块的状态),以模块Id 为 key,记录 [resolve、reject] 回调(确保模块加载合并完毕) 拼接 url,创建 script 标签,以 JSONP 的形式加载模块,然后执行 webpackJsonpCallback函数,合并模块代码到 modules`(modules是记录所有同步模块的变量),完毕后执行 resolve 回调 对 exports 对象做代理,返回 exports 对象 最后从 exports 对象身上拿到 default 函数,执行并打印结果


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3