8

首发地址: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的importexport语法,同时支持CommonJs的requireexports的语法,接下来将分别进行分析。

首先看"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对模块的转换,执行时的模块链接,以及模块动态加载的处理,在讲述过程中,尽可能的把涉及到的相关知识点都做了说明,有助于帮助读者巩固所学并学以致用。

    如果你是认真从开头读到这里,建议对本文讲解的内容有大概印象后,再结合完整代码梳理一遍,希望能够帮到你。有问题欢迎一起交流~

相关推荐

Tree Shaking原理 -【webpack系列】

ES6精读【划重点系列】(三)(模块化)

ES6精读【划重点系列】(一)(控制流程相关)

    


夜暮sky
97 声望5 粉丝