4.2 __webpack_require__.e函数
该函数主要作用就是创建一个<script>标签,然后将chunkId对应的文件通过该标签加载。
源代码如下:
1 __webpack_require__.e = function requireEnsure(chunkId) {
2 var promises = [];
3
4 // JSONP chunk loading for javascript
5
6 var installedChunkData = installedChunks[chunkId];
7 if (installedChunkData !== 0) { // 0 means "already installed".
8
9 // a Promise means "currently loading".
10 if (installedChunkData) {
11 promises.push(installedChunkData[2]);
12 } else {
13 // setup Promise in chunk cache
14 var promise = new Promise(function (resolve, reject) {
15 installedChunkData = installedChunks[chunkId] = [resolve, reject];
16 });
17 promises.push(installedChunkData[2] = promise);
18
19 // start chunk loading
20 var script = document.createElement('script');
21 var onScriptComplete;
22
23 script.charset = 'utf-8';
24 script.timeout = 120;
25 if (__webpack_require__.nc) {
26 script.setAttribute("nonce", __webpack_require__.nc);
27 }
28 script.src = jsonpScriptSrc(chunkId);
29
30 // create error before stack unwound to get useful stacktrace later
31 var error = new Error();
32 onScriptComplete = function (event) {
33 // avoid mem leaks in IE.
34 script.onerror = script.onload = null;
35 clearTimeout(timeout);
36 var chunk = installedChunks[chunkId];
37 if (chunk !== 0) {
38 if (chunk) {
39 var errorType = event && (event.type === 'load' ? 'missing' : event.type);
40 var realSrc = event && event.target && event.target.src;
41 error.message = 'Loading chunk ' + chunkId + ' failed.
(' + errorType + ': ' + realSrc + ')';
42 error.type = errorType;
43 error.request = realSrc;
44 chunk[1](error);
45 }
46 installedChunks[chunkId] = undefined;
47 }
48 };
49 var timeout = setTimeout(function () {
50 onScriptComplete({type: 'timeout', target: script});
51 }, 120000);
52 script.onerror = script.onload = onScriptComplete;
53 document.head.appendChild(script);
54 }
55 }
56 return Promise.all(promises);
57 };
主要做了如下几个事情:
1)判断chunkId对应的模块是否已经加载了,如果已经加载了,就不再重新加载;
2)如果模块没有被加载过,但模块处于正在被加载的过程,不再重复加载,直接将加载模块的promise返回。
为什么会出现这种情况?
例如:我们将index.js中加载print.js文件的地方改造为下边多次通过ES6的import加载print.js文件:
1 button.onclick = (
2 e => {
3
4 import('./print').then(
5 module => {
6 var print = module.default;
7 print();
8 }
9 );
10
11 import('./print').then(
12 module => {
13 var print = module.default;
14 print();
15 }
16 )
17 }
18 );
从上边代码可以看出,当第一import加载print.js文件时,还没有resolve,就又执行第二个import文件了,而为了避免重复加载该文件,就通过将这里的判断,避免了重复加载。
3)如果模块没有被加载过,也不处于加载过程,就创建一个promise,并将resolve、reject、promise构成的数组存储在上边说过的installedChunks缓存对象属性中。然后创建一个script标签加载对应的文件,加载超时时间是2分钟。如果script文件加载失败,触发reject(对应源码中:chunk[1](error),chunk[1]就是上边缓存的数组的第二个元素reject),并将installedChunks缓存对象中对应key的值设置为undefined,标识其没有被加载。
4)最后返回promise
注意:源码中,这里返回的是Promise.all(promises),分析代码发现promises好像只可能有一个元素。可能还没遇到多个promises的场景吧。留待后续研究。
4.3 自执行函数体代码分析
整个app.bundle.js文件是一个自执行函数,该函数中执行的代码如下:
1 var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; 2 var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 复制一个数组的push方法,这个方法的this是jsonpArray 3 jsonpArray.push = webpackJsonpCallback; // TODO: 为什么要复写push,而不是直接增加一个新方法名? 4 jsonpArray = jsonpArray.slice(); // 拷贝一个新数组 5 for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); 6 var parentJsonpFunction = oldJsonpFunction;
这段代码主要做了如下几个事情:
1)定义了一个全局变量webpackJsonp,改变量是一个数组,该数组变量的原生push方法被复写为webpackJsonpCallback方法,该方法是懒加载实现的一个核心方法,具体代码会在下边分析。
该全局变量在懒加载文件中被用到。在print.bundle.js中:
1 (window["webpackJsonp"] = window["webpackJsonp"] || []).push([ // 注意:这个push实际是webpackJsonpCallback方法 2 ["print"], 3 { 4 "./src/print.js": (function(module, __webpack_exports__, __webpack_require__) {...}) 5 } 6 ]);
2)将数组的原生push方法备份,赋值给parentJsonpFunction变量保存。
注意:该方法的this是全局变量webpackJsonp,也就是说parentJsonpFunction('111')后,全局数组变量webpackJsonp就增加了一个'111'元素。
该方法在webpackJsonpCallback中会用到,是将懒加载文件的内容保存到全局变量webpackJsonp中。
3)上边第一步中复写push的原因?
可能是因为在懒加载文件中,调用了复写后的push,执行了原生push的功能,因此,为了更形象的表达该意思,因此直接复写了push。
但个人认为这个不太好,不易读。直接新增一个_push或者extendPush,这样是不是读起来就很简单了。
4.4 webpackJsonpCallback函数分析
该函数是懒加载的一个比较核心代码。其代码如下:
1 function webpackJsonpCallback(data) { 2 var chunkIds = data[0]; 3 var moreModules = data[1]; 4 5 // add "moreModules" to the modules object, 6 // then flag all "chunkIds" as loaded and fire callback 7 var moduleId, chunkId, i = 0, resolves = []; 8 for (; i < chunkIds.length; i++) { 9 chunkId = chunkIds[i]; 10 if (installedChunks[chunkId]) { 11 resolves.push(installedChunks[chunkId][0]); 12 } 13 installedChunks[chunkId] = 0; 14 } 15 for (moduleId in moreModules) { 16 if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { 17 modules[moduleId] = moreModules[moduleId]; 18 } 19 } 20 if (parentJsonpFunction) parentJsonpFunction(data); 21 22 while (resolves.length) { 23 resolves.shift()(); 24 } 25 };
参数说明:
参数是一个数组。有两个元素:第一个元素是要懒加载文件中所有模块的chunkId组成的数组;第二个参数是一个对象,对象的属性和值分别是要加载模块的moduleId和模块代码函数。
该函数主要做的事情如下:
1)遍历参数中的chunkId:
判断installedChunks缓存变量中对应chunkId的属性值:如果是真,说明模块正在加载,因为从上边分析中可以知道,installedChunks[chunkId]只有一种情况是真,那就是在对应的模块正在加载时,会将加载模块创建的promise的三个信息搞成一个数组[resolve, reject, proimise]赋值给installedChunks[chunkId]。将resolve存入resolves变量中。
将installedChunks中对应的chunkId置为0,标识该模块已经被加载过了。
2)遍历参数中模块对象所有属性:
将模块代码函数存储到modules中,该modules是入口文件app.bundle.js中自执行函数的参数。
这一步非常关键,因为执行模块加载函数__webpack_require__时,获取模块代码时,就是通过moduleId从modules中查找对应模块代码。
3)调用parentJsonpFunction(原生push方法)将整个懒加载文件的数据存入全局数组变量window.webpackJsonp。
4)遍历resolves,执行所有promise的resolve:
当执行了promise的resolve后,才会走到promise.then的成功回调中,查看源码可以看到:
1 button.onclick = ( 2 e => { 3 __webpack_require__.e("print") 4 .then(__webpack_require__.bind(null, "./src/print.js")) 5 .then( 6 module => { 7 var print = module.default; 8 print(); 9 } 10 ) 11 } 12 );
resolve后,执行了两个then回调:
第一个回调是调用__webpack_require__函数,传入的参数是懒加载文件中的一个模块的moduleId,而这个moduleId就是上边存入到modules变量其中一个。这样就通过__webpack_require__执行了模块的代码。并将模块的返回值,传递给第二个then的回调函数;
第二个回调函数是真正的onclick回调函数的业务代码。
5)重要思考:
从这个函数可以看出:
调用__webpack_require__.e('print')方法,实际只是将对应的print.bundle.js文件加载和创建了一个异步的promise(因为并不知道什么时候这个文件才能执行完,因此需要一个异步promise,而promise的resolve会在对应的文件加载时执行,这样就能实现异步文件加载了),并没有将懒加载文件中保存的模块代码执行。
在加载对应print.bundle.js文件代码时,通过调用webpackJsonpCallback函数,实现触发加载文件时创建的promise的resolve。
resolve触发后,会执行promise的then回调,这个回调通过__webpack_require__函数执行了真正需要模块的代码(注意:如果print.bundle.js中有很多模块,只会执行用到的模块代码,而不是执行所有模块的代码),执行完后将模块的exports返回给promise的下一个then函数,该函数也就是真正的业务代码了。
综上,可以看出,webpack实际是通过promise,巧妙的实现了模块的懒加载功能。
5 懒加载构建原理图