首发地址:https://mp.weixin.qq.com/s/_r...
标题党:为了更好的讲解,文中代码占较多字数,不用担心~
通过本文你可以学到什么:webpack对模块化语法的支持
、打包后模块的链接执行
、动态加载模块的处理流程
,以及可能涉及到的诸多知识点等待你的发掘。提示:有些讲解放到了注释里,注意结合一起看。
正文开始~
目前前端工程通过webpack的构建,支持ESModule和CommonJs的写法,达到前端模块化的需求。本文将对webpack的模块链接以及动态获取模块的实现方式做深入分析。并对涉及到的知识点,比如treeShaking、CommonJs实现、闭包应用都有提及,方便大家真正知道其实现原理,想吃快餐的童鞋可以选择性速读。
在接下来的例子中,将同时使用ESModule和CommonJs的语法导入导出,尽量全面的刨析webpack做的工作,代码如下:
//index.js 入口文件
import _, { name } from './es';
let co = require('./common');
co.sayHello(name);
export default _;
//es.js
export const age = 18;
export const name = "前端事务所";
export default "ESModule";
//common.js
exports.sayHello = (name, desc) => {
console.log(`欢迎关注[前端事务所]~`);
}
为了更完整的展示webpack的处理细节,使用的webpack配置需要说明下:
- 由于mode:production默认开启了作用域提升,这里采用mode:development模式;
- 未使用压缩插件。
-
output.filename:'js/[name].[hash].js'
//默认name为main
打包后代码如下(略长,回顾梳理用,先别管,往下翻!:
//main.9993bb.js
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// 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;
}
// 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 = "/";
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = 0);
})({
"./src/index.js":
/*! exports provided: default */
/*! all exports used */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */
var _es__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./es */ "./src/es.js");
var co = __webpack_require__(/*! ./common */ "./src/common.js"); // import co from './common';
co.sayHello(_es__WEBPACK_IMPORTED_MODULE_0__[/* name */ "b"]);
/* harmony default export */
__webpack_exports__["default"] = (_es__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"]);
}),
"./src/es.js":
/*! exports provided: age, name, default */
/*! exports used: default, name */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* unused harmony export age */
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, "b", function() { return name; });
var age = 18;
var name = "前端事务所";
/* harmony default export */
__webpack_exports__["a"] = ("ESModule");
}),
"./src/common.js":
/*! no static exports found */
/*! all exports used */
(function(module, exports) {
exports.sayHello = function (name, desc) {
console.log("\u6B22\u8FCE\u5173\u6CE8[\u524D\u7AEF\u4E8B\u52A1\u6240]~");
};
}),
0:
(function(module, exports, __webpack_require__) {
module.exports = __webpack_require__(/*! */"./src/index.js");
})
});
//# sourceMappingURL=main.6196cc781843c8696cda.js.map
貌似有点长,很多人可能会被劝退,那么精简后如下:
(function(modules) { // webpackBootstrap
//...
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = 36);
})({
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {/*模块内容*/}),
"./src/es.js":
(function(module, __webpack_exports__, __webpack_require__) {/*模块内容*/}),
"./src/common.js":
(function(module, exports) {/*模块内容*/})
});
//# sourceMappingURL=main.6196cc781843c8696cda.js.map
这样就好多了,从上面我们可以大致获取如下信息:
1)我们的模块被转换成了立即执行函数表达式(IIFE),函数会自执行,进行模块的创建以及链接等;(runtime代码)
2)所有模块组装成对象作为函数的参数传入。对象的构成:{ [文件的路径]:[被包装后的模块内容] }
;
3)每个模块都被构造的函数包裹。(模块代码的转换)
下面我们对上面代码做拆解,分别对"模块如何被封装"以及"怎么被链接起来",做详细分析。
模块代码的转换
由于webpack不仅支持ESM的import
和export
语法,同时支持CommonJs的require
和exports
的语法,接下来将分别进行分析。
首先看"ESM规范"的代码会被如何处理:
"./src/es.js":
/*! exports provided: age, name, default */
/*! exports used: default, name */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* unused harmony export age */
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, "b", function() { return name; });
var age = 18;
var name = "前端事务所";
/* harmony default export */
__webpack_exports__["a"] = ("ESModule");
})
从上到下分析:
1)每个函数上面会有一些注释:
//记录了当前模块被导出的接口有哪些
exports provided
//导出接口中被使用的有哪些
exports used
函数体内部同样有类似的注释:
// 未被使用的导出
unused harmony export age
// 被使用的导出,(type:使用类型)
harmony export (binding)
正是基于这些接口信息的记录,使得webapck可以实现treeShaking,具体细节参考Tree Shaking原理。
2)函数的参数分别为:module, __webpack_exports__, __webpack_require__
Tips:
有一点需要强调下,CommonJs中导出的是模块对象,ESM规范中模块是不存在模块对象的,因此可以使用this===undefined判断当前环境是否为模块内。但是webpack支持ESM语法是通过将其转换成类似CommonJs的加载形式,只不过模块的读取不是通过文件流从硬盘读取,而是从内存读取(CommonJs的简单实现可以参考模块化介绍)。
这里module就是模块对象,而__webpack_exports__ === module.exports,这点和CommonJs一致,既然这样,只传一个module不就行了?CommonJs是支持module.exports和exports两种写法的,相同实现。
Webpack使用__webpack_require__对import关键词的做了替换处理。
3)可以看到导出的age、name 被转换成变量声明,那怎么导出的呢?
可以看到有个__webpack_require__.d方法,看一下它做了什么事(看注释):
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
//判断该变量是否已经挂载到exports上
if(!__webpack_require__.o(exports, name)) {
//给exports添加导出变量(接口),可枚举,通过getter读取自定义的值
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
哦,原来是将导出的变量绑定到exports上。由于age被标记为未使用,因此并不会被注册到exports上。而通过使用__webpack_require__.d将name绑定到exports,这里需要注意三点:
- 通过闭包传引用的方式(getter的实现)是为了实现ESM的规范:导出不是值的复制,而是共享的引用。使用时exports.name,会触发getter返回其当前值;
- 显式的为对象属性添加属性描述器{enumerable: true, get: getter},由于set:undefined,严格模式("use strict")下,不能更改其值。
- 细心的童鞋会发现,__webpack_require__.d是被放到模块顶层首先被执行的,为什么呢?为了实现ESM的export的提升(变量|函数的区别,参见模块化中ES的循环依赖部分),注意和后面介绍的模块初始化时放入缓存一起理解。
另外__webpack_exports__["a"] = ("ESModule");,这个a其实就是default,也是被挂载到module.exports上。和name接口导出定义的区别,在于default可以重新赋值,如:
import * as ES from './a.js'
ES.name = 1; //Uncaught TypeError: Cannot set property name of #<Object> which has only a getter
ES.default = 123; //ok
这就是ESM导出的关键部分,基本涵盖了常见的处理场景,也并没有那么神秘~
至此,涉及ESM的export部分大致就这些,接下来看下Webpack是如何处理CommonJs的导出的,代码如下:
"./src/common.js":
/*! no static exports found */
/*! all exports used */
(function(module, exports) {
exports.sayHello = function (name, desc) {
console.log("\u6B22\u8FCE\u5173\u6CE8[\u524D\u7AEF\u4E8B\u52A1\u6240]~");
};
})
可以看到模块代码基本没有变化,同样是被封装到了函数内,传入module对象,模拟了CommonJs的实现。不再赘述。
最后看下入口文件index.js是如何导入模块的:
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */
var _es__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./es */ "./src/es.js");
var co = __webpack_require__(/*! ./common */ "./src/common.js");
co.sayHello(_es__WEBPACK_IMPORTED_MODULE_0__[/* name */ "b"]);
/* harmony default export */
__webpack_exports__["default"] = (_es__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"]);
})
__webpack_require__暂且知道其作用是返回模块对象就可以了。其中__webpack_require__.r是什么?
// 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 });
};
主要作用是为当前打包的模块做个标记,表明这是个符合ESM规范导出的模块;一般三方包使用时,import方式获取其默认导出时,可以看到被webpack打包后的代码中就是通过__esModule这个标记判断是否为ESM导出,然后返回合适的值作为default接口值。
【扩展】本文的例子中对common.js文件模块的导入使用的是let co = require('./common.js');
同样可以使用import co from './common.js';
的方式,打包后CommonJs模块导入转换代码变成如下形式:
var _common__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./common */ "./src/common.js");
先看下__webpack_require__.n对获取的模块做了什么事:
// 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;
};
可以看到,__webpack_require__.n主要是获取模块的default导出。判断模块是否为__esModule,是则直接返回module['default']值,不是则返回模块对象。因为CommonJs规范中,模块的默认导出就是module对象自身。(这里说的module即__webpack_require__.n(module)的参数,即module.exports对象,不要混淆)。
那为什么获取es.js
中的default不使用__webpack_require__.n获取默认值_
呢?_webpack_require__.n中实际是将default值作为a属性的读取器返回值;再看es.js中的导出default直接使用a绑定exports,使用时也是使用a获取。因为是内部ESM,webpack构建时是清楚模块的导出方式的,所以省略了中间步骤。
模块代码的转换至此结束,那么模块在浏览器怎么执行的呢?接下来将对模块是怎么链接起来的做分析。
模块代码的链接(runtime)
打包好后,会有一段webpack的启动程序随着模块信息一起被浏览器加载,控制模块的创建和执行。前面讲完了IIFE的参数部分,下面介绍其函数体(启动代码):
(function(modules) { // webpackBootstrap
//模块缓存,单例模式
var installedModules = {};
// import 和 require 会被替换为__webpack_require__
// 用于创建、加载模块
function __webpack_require__(moduleId) {
// 模块已被缓存时,直接返回模块对象
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建新的m模块对象 | 放入缓存【注意循环依赖时】
var module = installedModules[moduleId] = {
i: moduleId,
l: false, //是否已加载完成
exports: {}
};
//将module, module.exports, __webpack_require__传入封装的函数,执行函数
// 每个参数上面已经介绍过了
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;
}
/*
* ...
* 挂载在__webpack_require__上的一些辅助函数,其中前面介绍了
* __webpack_require__.d
* __webpack_require__.r
* 不再赘述,有兴趣可以自己看下
*/
// 即先执行参数对象中key为o的函数,该函数仅用于加载入口模块
// 0:
// (function(module, exports, __webpack_require__) {
// module.exports = __webpack_require__(/*! */"./src/index.js");
// })
return __webpack_require__(__webpack_require__.s = 0);
})
这部分建议结合注释阅读代码。好了,常规的模块的转换以及runtime(运行时)函数已经介绍完了~
这就完了?对webpack支持import()动态加载,这是怎么实现的呢?
import()
使用的示例如下:
//index.js 入口文件
import(/* webpackChunkName: "es" */ './es.js')
.then((val => console.log(val)))
//es.js
export const name = "前端事务所";
export default "ESModule";
打包后发现,除了生成main.js还多出来一个es.js,es.js即待使用import()懒加载的模块,其代码如下:
//webpackJsonp:[['模块名(Id)',{'模块路径':模块执行函数}]]
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["es"],{
"./src/es.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "name", function() { return name; });
var name = "前端事务所";
__webpack_exports__["default"] = ("ESModule");
})
}]);
我们知道入口模块打包后组成的对象是作为runtime函数的参数,而异步获取的模块采用同样形式的封装,push进webpackJsonp数组中或者立即执行(webpackJson.push是原型链上的push还是重新的回调函数,由异步模块和runtime执行顺序决定,这是后话。另外模块代码上面已经详细做了介绍,这里不再赘述)。
再来看main.js中的runtime的部分代码(这里只列出几处关键代码):
(function(modules) {
**** 关键代码3:异步模块获取的回调函数 ****
// install a JSONP callback for chunk loading
functionwebpackJsonpCallback(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(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()();
}
};
**** 关键代码1:异步请求模块 ****
__webpack_require__.e = functionrequireEnsure(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);
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;
var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
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);
};
****关键代码2:回调函数的注册 | 已注册模块的执行****
varjsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
varoldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(vari = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
varparentJsonpFunction = oldJsonpFunction;
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = 0);
})({
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
****关键代码4:import()的转换****
__webpack_require__.e(/*! import() | es */ "es").then(__webpack_require__.bind(null, /*! ./es.js */ "./src/es.js")).then(function (val) {
return console.log(val);
});
}
});
})
上面列出了4处关键代码,按顺序解读下:
import()会被转换成__webpack_require__.e(/*! import() | es */ "es"),先看下__webpack_require__.e的代码:
// JSONP chunk loading for javascript
__webpack_require__.e = functionrequireEnsure(chunkId) {
var promises = [];
var installedChunkData = installedChunks[chunkId];
//先判断当前模块是否已经加载完成(加载完成后会将其标识为0)
if(installedChunkData !== 0) {
// 如果不为undefined,意味这该模块的请求已发出,loging状态
if(installedChunkData) {
//如果该模块处于loading中,则将模块缓存中记录的promise存入promises
promises.push(installedChunkData[2]);
} else {
// 创建一个新的Promise,用installedChunks记录下该模块返回Promise的resolve和reject
//便于处理好后正确执行.then (Promise相关可参考【前端事务所】ES6系列一)
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
//installedChunkData[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);
}
script.src = jsonpScriptSrc(chunkId);
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
//...
}
installedChunks[chunkId] = undefined;
}
};
//异步模块加载超时处理
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
//模块加载完成(失败|成功)做的处理:清除定时器等
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
//import()执行返回Promise实例,promises记录了模块请求时被缓存的promise
// 当模块加载完成后,promise的状态变更,会通知到所有请求了该模块的import()回调 执行
// 等价于为模块缓存额promise注册了多个回调函数:按顺序执行:
// p.then(A动态加载请求回调) | p.then(B动态加载请求回调)
return Promise.all(promises);
};
再简单介绍下(文中注释部分已经解释的比较清楚):
1)创建一个Promise对象,使用installedChunks记录下其resolve和reject,便于后面获取资源后切换上下文,控制.then()的执行时机;
2)installedChunks用于记录已加载和加载中的chunk;
// installedChunks[模块名]的值:
undefined = chunk还没加载;
null = prefecth/preload;
Promise = chunk 加载中;
0 = chunk加载完成。
3)然后创建Script标签,发起异步请求(requireJs也是类似方式做的异步加载)
前面已经知道异步加载模块的代码形式,模块加载完成后,异步模块如果早于runtimeChunk执行,则被塞到window["webpackJsonp"]中等待webapck回调函数执行;如果迟于runtimeChunk执行,此时window["webpackJsonp"].push即为webpackJsonpCallback回调函数,立即执行。先看下面的代码:
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(vari = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
1)判断window["webpackJsonp"]是否已被创建;
这里解释下,比如通过配置optimization.runtimeChunk:true,将runtime运行时代码单独打包,会生成runtime和main.js两个js文件,且都是以script的方式插入index.html,(main.js打包后的代码形式和es.js相同)这样的话,哪个文件先加载完不一定,所以需要考虑这种情况。
回顾下流程控制解决方案:函数嵌套、Promise、Generator。上面采用的解决方案类似于thunk控制自动执行的实现:
function thunkFun() {
let val;
//模拟异步操作
setTimeout(() => {
let msg = '前端事务所';
if(val) {
val(msg); return;
}
val = msg;
}, 1000)
return (callback) => {
if(!val) {
val = callback; return;
}
callback(val);
}
}
//使用
let run = new thunkFun();
let callback = val => console.log(val);
//情况1:模拟先注册回调,而后异步操作才执行完
run(callback);
//情况2:模拟异步操作先执行完,而后回调函数才注册
setTimeout(() => {
run(callback)
}, 3000);
正是基于这种自动执行权的切换,使得其和Promise一样可以作为Generator的自执行器,参见流程控制详解;用过react的应该知道redux-thunk。另外注意区分thunk(强调了传入回调函数)和柯里化。
2)如果异步模块优先加载完成,则缓存到webpackJsonp数组中等待调用webpackJsonpCallback执行;
3)如果runtime代码先加载完成,则会重写webpackJsonp.push,等到异步模块加载完成后执行,等同于直接调用webpackJsonpCallback回调。
我们直接看下webpackJsonpCallback异步回调函数对模块做了什么事:
function webpackJsonpCallback(data) {
var chunkIds = data[0]; //模块名称(id)
var moreModules = data[1]; //模块
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId]) {
//收集异步模块的installedChunks中记录的resolve
//创建script请求时,installChunks记录了该模块对应的值:
//[resolve, reject, promise]
resolves.push(installedChunks[chunkId][0]);
}
// 标记当前模块已经加载完成
installedChunks[chunkId] = 0;
}
//注册到全局modules:[]上(全部模块都会被集中收集在modules中)
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
//webpackJsonp数组的原型链上的push方法
if(parentJsonpFunction) parentJsonpFunction(data);
while(resolves.length) {
//最后触发触发promise注册的回调函数
// import().then() 等价于 Promise.all(promises).then()
resolves.shift()();
}
};
简单介绍下(配合注释看代码):
1)将异步加载的模块标记为加载完成:installedChunks[chunkId] = 0;(不要忘记创建script异步获取模块时,设置了setTimeout和onScriptComplete做超时和异常捕获)
2)将异步加载的模块先缓存在全局维护的modules中(下载后并不立即执行模块内容),真正执行由调用__webpack_require__时负责;
3)由于installedChunks中记录了模块对应的[resolve, reject, promise],加载完成后调用resolve,触发回调函数。
现在,异步模块信息已经被记录,并且通过installedChunks触发resolve执行__webpack_require__.e的回调函数:
__webpack_require__.e(/*! import() | es */ "es")
.then(__webpack_require__.bind(null, /*! ./es.js */ "./src/es.js"))
.then(function (val) {
return console.log(val);
});
前面介绍了,异步模块下载后会被存于modules中。可以看到__webpack_require__.e返回的promise的回调函数即__webpack_require__,它通过modules找到模块然后执行并返回modules.exports。
现在我们知道了,异步模块在下载后先将模块缓存到modules中,同时触发promise 回调的执行,真正创建和获取模块对象是在回调函数中通过__webpack_require__完成的;而不是获取后立即创建模块对象,然后通过调用resolve(module.exports)给promise的回调函数传递模块对象。
至此import()动态加载的流程走完。
总结
runtimeChunk单独打包本文有提及,没有详细举例展开,不过原理差不多,简单理解就是把main.js中的所有模块,通过回调注册到runtimeChunk中的moudles上。
本文基本涵盖了webpack对模块的转换,执行时的模块链接,以及模块动态加载的处理,在讲述过程中,尽可能的把涉及到的相关知识点都做了说明,有助于帮助读者巩固所学并学以致用。
如果你是认真从开头读到这里,建议对本文讲解的内容有大概印象后,再结合完整代码梳理一遍,希望能够帮到你。有问题欢迎一起交流~
相关推荐
ES6精读【划重点系列】(三)(模块化)
ES6精读【划重点系列】(一)(控制流程相关)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。