从立即执行函数开始
首先定义一个函数,这个函数的入参为打包的入口./src/index.js
,webpack是用相对资源地址作为模块的键值。
(function(modules) {})(
{
"./src/index.js":
(function(module, exports, __webpack_require__) {
const page = () => {
return __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.t.bind(null, "./src/page.js", 7))
}
})
}
)
代码首先执行立即执行函数,传入的参数modules为一个键值对形式。每个模块的加载函数接收三个参数:
- module是webpack对模块的抽象,它的数据结构有模块id(i),模块是否加载(l),模块的导出对象(exports)。在webpack中万物皆模块。
- exports,为模块的导出。
- __webpack__require__为模块加载函数,异步的或同步的。
__webpack_require__
// 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;
/******/ }
这个函数接收一个moduleId
,模块id参数。
- 检查模块是否在installedModules的缓存里
- 如果没有对应moduleId的模块,则创建一个模块。将这个模块的外包装对象分别赋值给
module
变量和installedModules[moduleId]
,这样缓存里就存在了这个模块id的缓存。 - i表示这个模块的id,
- l表示模块是否被加载了
- exports表示这个模块
modules['./src/index.js']
就会执行,上下文为module.exports
,第一个参数模块就是module
(模块的包装对象), 第二个参数为真正的模块,第三个参数为__webpack_require__函数本身。由于page.js是一个异步模块,会调用__webpack_require__的异步模块加载程序。
异步模块加载requireEnsure
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // Promise = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "main": 0
/******/ };
异步加载模块,首先定义一个安装的块集合变量,默认main.js模块是加载完成的状态,异步模块加载有四个过程:
- undefined表示异步模块未被加载
- null表示异步模块被预加载
- Promise表示模块正在加载中
- 0表示模块已加载完成 在Promise状态,我们可以在模块渲染的地方显示一个模块加载的动画,目前webpack模块加载的机制还没整明白,我们继续往下看。
chunkId
每个异步模块有一个chunkId。比如./src/page.js
。通过__webpack_require__的异步模块加载函数,我们异步加载了page.js内容。
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/page.js":
(function(module, exports) {
function component() {
const element = document.createElement('div');
element.innerHTML =['Hello', 'webpack'].join(',')
return element;
}
document.body.appendChild(component());
})
}]);
看看这个异步模块的内容,webpackJsonp是一个全局变量,它是一个数组,数组的元素是数组(用moduleX表示),moduleX的第一个元素是一个数组,第二个元素是一个模块表,存储各个模块。 具体为什么这样的结构,还需要看后续的代码。webpackJsonp维护着一个模块表。
// This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = function requireEnsure(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]);
/******/ }
/******/ }
/******/ return Promise.all(promises);
/******/ };
- 这个函数使用了Promise异步api
- installedChunkData表示异步模块的加载状态。如果加载状态不等于0,则执行模块加载的流程。根据上述所说的四个状态进行处理。
- 0, null,undefined均为falsy的值,因此只有
Promise
的状态下才进入if(installedChunkData)
分支。
创建加载js异步Promise的流程
else {
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise(function(resolve, reject) {
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject];
/******/ });
首先创建一个promise
,promise
执行立即变更installedChunkData
状态的值为Promise
状态
,并将缓存模块状态的值变为一个包含resolve,reject的数组。这里有一个疑问,为什么不将promise直接赋值给installedChunkData呢?继续看代码:
promises.push(installedChunkData[2] = promise);
这里直接将安装模块的索引为2的元素变为promise。这里的问号更大了。
script.onerror = script.onload = onScriptComplete;
统一处理script加载错误或成功。
var timeout = setTimeout(function(){
/******/ onScriptComplete({ type: 'timeout', target: script });
/******/ }, 120000);
如果onScriptComplete
没有调用,则会在未来的一个时间段爆出错误。超时时间2分钟。也就是说一个异步模块如果2分钟内没有加载完成,则视为这个模块加载失败。那么我们来看看onScriptComplete
这个函数。
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;
/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
/******/ error.name = 'ChunkLoadError';
/******/ error.type = errorType;
/******/ error.request = realSrc;
/******/ chunk[1](error);
/******/ }
/******/ installedChunks[chunkId] = undefined;
/******/ }
/******/ };
首先清空onerror
和onload
,避免ie的内存泄露,同时清除定时器。
如果chunk不是加载完成的状态,则表示异步模块未加载成功。会抛出ChunkLoadError
错误。这个错误一般是js
和css
的404
。
那么疑问是,怎样的状态才会执行installedChunks[chunkId] = 0
呢?
// script path function
/******/ function jsonpScriptSrc(chunkId) {
/******/ return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".js"
/******/ }
jsonScriptSrc这个函数用于获取chunk的路径。仅仅是获取路径。 异步模块加载完成后,比如page.js。都会执行一个叫webpackJsonpCallback的函数,为什么,因为webpackJson数组的push方法被webpackJsonpCallback劫持。在想webpackJsonp这个变量推入更多模块时将会执行更多操作。
/******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
/******/ jsonpArray.push = webpackJsonpCallback;
/******/ jsonpArray = jsonpArray.slice();
/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
/******/ var parentJsonpFunction = oldJsonpFunction;
/******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
/******/ jsonpArray.push = webpackJsonpCallback;
/******/ jsonpArray = jsonpArray.slice();
/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
/******/ var parentJsonpFunction = oldJsonpFunction;
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/index.js");
首先定义了一个jsonpArray
数据,同时将值赋值给window
的webpackJsonp
属性。
更改数组的push
方法为webpackJsonpCallback
,用以在push
模块时执行更多的操作。
jsonpArry[i]
返回一个一个数组,敲重点。这就是为什么异步模块push
的时候数据结构是一个数组,数组第一个元素是数组,第二个是模块表。看下面代码回忆一下:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/page.js":
(function(module, exports) {
function component() {
const element = document.createElement('div');
element.innerHTML =['Hello', 'webpack'].join(',')
return element;
}
document.body.appendChild(component());
})
}]);
就是webpackJsonpCallback需要用到这种数据结构。 首先数组的第一个元素表示chunkIds。 第二个参数为一个对象,表示更多的模块。 然后把所有chunkId的加载状态改为0,已加载状态。 将解析出的模块放入解析数组。并一个个执行。 我们这里要理解几个变量是干嘛的:
- installedChunks,前面说过,保存模块加载状态。
- resolves,每个模块的执行函数
- modules,用来保存所有的模块。
- parentJsonpFunction,数组的push方法,就是将模块推入webpackJsonp这个数组变量。 读到这里,我们对整个运行流程可能还是模糊的。不过,前面的阅读是为了后面的断点调试做准备。
/******/ function webpackJsonpCallback(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()();
/******/ }
/******/
/******/ };
断点解读
真正理解源码的最好办法是断点运行。
我们看到,代码主入口,立即执行函数传入了一个modules变量。这个模块表只有一个注册值,'./src/index.js'也就是项目的打包入口,这算是一个总的模块。接着初始化了普通变量和函数变量,直接执行到return语句。
return __webpack_require__(__webpack_require__.s = "./src/index.js");
这里代码项__webpack_require__
函数传入了一个moduleId
,'./src/index.js'
。
这个installedModules
还是一个空对象,这样就进入了创建一个包装模块的流程。执行入口模块的加载函数
这就是index.js
的代码执行。
当我们需要请求一个异步模块page.js时,我们创建了一个script
标签用于加载0.js
,0
就是这个异步块的chunkId
。
这样我们就执行到了webpackJsonpCallback,这个劫持操作就是把异步模块放入modules
变量。
当resolves执行后,Promise的then操作就会执行。
__webpack_require__.t.bind(null, /*! ./page */ "./src/page.js", 7)
这操作就是执行了异步模块的内容。这样,这个异步模块的内容就展示在页面上了。 感谢阅读。