前言

前端性能优化方面, 很多时候我们需要减小主包大小, 提升首屏加载渲染时间, 这时候我们就需要异步的延迟加载模块,webpack提供了运行时加载模块函数 import() , import函数的作用就是 动态的加载模块。调用 import 地方,被视为分割点,意思是,被请求的模块和它引用的所有子模块,会分割到一个单独的 chunk 中

详情可以看webpack官方对import()的使用方法解释 传送门

import() 具体做了什么 我们先搭一个demo

搭建DEMO

mkdir webpack-import-demo
cd webpack-import-demo
yarn init -y
yarn add webpack webpack-cli html-webpack-plugin --dev

根目录下创建配置文件 webpack.config.js

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');


module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist'),
        clean: true,
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'webpack-import-demo',
        }),
    ],
};

package.json 中添加scripts 省的每次都敲一堆命令行

"scripts": {
    "build": "webpack --mode=development --config webpack.config.js"
  },

然后 根目录创建scr/index.js设置定时器 3秒后去加载./dynamic.js 并执行他

// scr/index.js
setTimeout(()=>{
    import('./dynamic').then(({default:text}) => {
        alert(text)
    })
},3000)

创建src/dynamic.js

// src/dynamic.js
const text = '我是运行时异步载入的文本'

export default text

然后我们yarn build 看下效果 2021-10-21 16.02.31.gif

OJBK 编译完成 我们在浏览器打开index.html康康 2021-10-21 16.14.53.gif

good good 3秒后才去加载 ./dynamic.js并执行他

让我们康康编译后的代码是啥样 做了些啥

打开dist/mian.js 让我们研究下他是怎么做的

草!(一种植物) 这么多 还有eval 这怎么读!!

我们在webpack.config.js中添加devtool: false
image.png

然后重新执行 yarn build 打开dist/mian.js

good good 终于读起来通顺了

开始分析

main.js

可以看到 这个文件其实就是一个IIFE(自执行函数),上面定义了一堆webpack运行时用的各种函数等等,最后面就是编译后我们main.js中的内容了,
我们写的 import('./dynamic')被编译成了__webpack_require__.e(/*! import() */ "src_dynamic_js").then(__webpack_require__.bind(__webpack_require__, /*! ./dynamic */ "./src/dynamic.js")) image.png

我们开始往上找 先看看__webpack_require__ 是干啥的

__webpack_require__
/******/     // The require function
/******/     function __webpack_require__(moduleId) {
/******/         // Check if module is in cache
/******/         var cachedModule = __webpack_module_cache__[moduleId];
/******/         if (cachedModule !== undefined) {
/******/             return cachedModule.exports;
/******/         }
/******/         // Create a new module (and put it into the cache)
/******/         var module = __webpack_module_cache__[moduleId] = {
/******/             // no module.id needed
/******/             // no module.loaded needed
/******/             exports: {}
/******/         };
/******/     
/******/         // Execute the module function
/******/         __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/     
/******/         // Return the exports of the module
/******/         return module.exports;
/******/     }

搜嘎 原来是通过模块ID查找模块的函数, 如果模块在缓存中就直接从缓存中返回 , 如果不在缓存中那就创建一个并且执行__webpack_modules__[moduleId]这个方法 然后返回这个模块

我们再看看__webpack_require__.e是啥

__webpack_require__.e

继续查找 __webpack_require__.e的定义

/******/     /* webpack/runtime/ensure chunk */
/******/     (() => {
/******/         __webpack_require__.f = {};
/******/         // This file contains only the entry chunk.
/******/         // The chunk loading function for additional chunks
/******/         __webpack_require__.e = (chunkId) => {
/******/             return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/                 __webpack_require__.f[key](chunkId, promises);
/******/                 return promises;
/******/             }, []));
/******/         };
/******/     })();

原来 __webpack_require__.e返回了一个promise, 再看里面 Object.keys(__webpack_require__.f), 提取 f对象上的key,但是我们顺着作用域往上找 看到__webpack_require__.f = {} , 什么! 是个空对象?
心机之蛙一直摸你肚子!
那他一定在之前对 __webpack_require__.f 有添加属性!不然要这个Object.keys干嘛! 继续顺着找, 果然! image.png 他在__webpack_require__.f上添加了一个名为 'j'的函数, 这个函数接收一个chunkId和一个promises, 主要作用就是判断 installedChunks 中有没有包含[chunkId]这个key 如果米有加载过 那就成一个promise,并且把[resolve, reject]赋值给installedChunkData ,然后通过__webpack_require__.l大概就是生成一个script标签添加进head,以JSONP的方式开始加载这个资源,执行完就把这个script 删掉 image.png
但是!!!!!! 这个promise的resolve啥时候执行的??? 加载资源的回调函数loadingEnded中没有执行 resolve`啊! 这代码跑不通了啊 找!!

image.png

哈啊!在这呢 原来在webpackJsonpCallback中执行的呀

那继续找webpackJsonpCallback在哪执行的

找到了 image.png

他在全局定义了一个参数 self["webpackChunkwebpack_import_demo"] (self也就是window对象),并且重写了push方法 这样在调用self["webpackChunkwebpack_import_demo"].push的时候webpackJsonpCallback也就执行了! 那这个push在哪调用的呢??

可以看到 运行到这的时候 加载了src_dynamic_js.main.js image.png

src_dynamic_js.main.js
"use strict";
(self["webpackChunkwebpack_import_demo"] = self["webpackChunkwebpack_import_demo"] || []).push([["src_dynamic_js"],{

/***/ "./src/dynamic.js":
/*!************************!*\
  !*** ./src/dynamic.js ***!
  \************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
const text = '我是运行时异步载入的文本'

/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (text);


/***/ })

}]);

可以看到这个js也是个IIFE 哎 这self["webpackChunkwebpack_import_demo"].push 这不就调用了吗 传入了一个数组,数组第一项为['./src/dynamic.js']第二项为一个对象 对象内有一个方法 方法名叫'./src/dynamic.js'

ok 再回到main.js

再看webpackJsonpCallback接收两个参数 parentChunkLoadingFunction和data 第一个参数不用管 这个函数是柯里化过 就是chunkLoadingGlobal ,柯里化不懂的不用管他,我们只需要知道 data就是我们./src/dynamic.js在执行push的时候传的值就行
然后对data解构

var [chunkIds, moreModules, runtime] = data;

moreModules就是传入的那个对象, 然后用 for...in对其遍历,把moreModules本身含有的所有键值都复制给__webpack_require__.m也就是__webpack_modules__,他俩是一个对象, 因为moreModules中只有一个'./src/dynamic.js'方法,所以此时, __webpack_require__.m = __webpack_modules__ = { ' ./src/dynamic.js' : function( xxx ){ xxxx }},

终于!!! __webpack_require__.e(/*! import() */ "src_dynamic_js")这段跑完了,模块注册完成了,他的then可以执行了

then里面执行 __webpack_require__ 传入了 ./src/dynamic.js这个key 然后__webpack_modules__['./src/dynamic.js'](module, module.exports, __webpack_require__)执行, 执行中通过 __webpack_require__.ddefault定义到module.exports 并设置 getter,然后把module.exports返回出去

image.png

此时,最后那个then就能获取到模块的返回了!!!!!

收工

目录