11111111--临时保存

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.\n(' + 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 懒加载构建原理图

 

posted on 2019-06-03 21:42  ゛墨メ冰ミ  阅读(332)  评论(0编辑  收藏  举报

导航