webpack 懒加载(异步加载)原理 |
您所在的位置:网站首页 › webpack基础配置插件 › webpack 懒加载(异步加载)原理 |
在 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 后,得到如下的文件: 其中,src_a_js.js 就是通过 import("./a") 懒加载的模块 我们在浏览器中打开 index.html,打开控制台: 此时只加载了 main.js,且控制台没有任何输出。当我点击按钮后,才会执行 src_a_js.js 模块,控制台打印 Jolyne 我们打包代码后,在 ./dist/main.js 里面能找到如下的代码: 我们发现他会分为三个步骤: 调用 __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 |