阅读 402
Webpack 懒加载源码逐行解读 探究原理与实现

Webpack 懒加载源码逐行解读 探究原理与实现

懒加载原理

webpack懒加载其实就是一个很简单的概念:动态标签,可以理解为就是JSONP,原理也就是动态插入script标签,Just this!!!

首先让我们先看看JSONP的实现

(function (window,document) {
    "use strict";
    var jsonp = function (url,data,callback) {

        // 将传入的data数据转化为url字符串形式
        var dataString = url.indexof('?') == -1? '?': '&';
        for(var key in data){
            dataString += key + '=' + data[key] + '&';
        };

        // 处理url中的回调函数
        var cbFuncName = 'cb' + Math.random().toString().replace('.','');
        dataString += 'callback=' + cbFuncName;

        // 创建一个script标签并插入到页面中
        var script = document.createElement('script');
        script.src = url + dataString;

        // 挂载回调函数
        window[cbFuncName] = function (data) {
            callback(data);
            // 处理完回调函数的数据之后,删除jsonp的script标签
            document.body.removeChild(script);
        }

        // append到页面中
        document.body.appendChild(script);
    }

    window.jsonp = jsonp;

})(window,document)
复制代码

webpack懒加载实现

  • webpack的import的加载源码
/**
* @desc webpack是动态加载script的函数
* @param chunkId 为webpack打包自动生成的序号, 可由魔法注释 webpackChunkName修改打包后的命名
*                 import(/* webpackChunkName: 'Lazy' */'./Lazy') => Lazy.main.js
*/
__webpack_require__.e = function requireEnsure(chunkId) {
  var promises = [];

  // installedChunks是webpack内部维护的依赖安装的变量
  var installedChunkData = installedChunks[chunkId];

  // 0 代表已安装, 如果在缓存中不存在, 则进入以下流程
  if(installedChunkData !== 0) { // 0 means "already installed".

    // 如果在缓存中存在, 但是状态不等于0, 则说明还在安装过程中
    if(installedChunkData) {
      promises.push(installedChunkData[2]);
    } else {
      // 将依赖包装成一个promise推到缓存中, 代表已经有加载过这个依赖了
      var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      // ∨∨∨ 注意这一步, 这里推入的promises, 后续会在JsonCallback中执行 ∨∨∨
      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 ∨∨∨
      script.src = jsonpScriptSrc(chunkId);

      // create error before stack unwound to get useful stacktrace later
      var error = new Error();
      // script加载成功或失败的回调
      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;
        }
      };
      // 超时处理
      var timeout = setTimeout(function(){
        onScriptComplete({ type: 'timeout', target: script });
      }, 120000);
      script.onerror = script.onload = onScriptComplete;
      // 在head插入script标签
      document.head.appendChild(script);
    }
  }
  return Promise.all(promises);
};

复制代码

对比了JSONP的实现, 是不是发现很类似, 也就多了步缓存处理

  • webpackJsonpCallback源码
...
// webpack 在window下注册的一个webpackJsnop 事件,并重写了这个方法的push事件, 
// 每次懒加载都会触发这个push事件
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
...

=======================================================================================
// 我们先看下import(/* webpackChunkName: 'Lazy' */'./Lazy')中的Lazy最后会打包成什么样子
@file dist/lazy.main.js
// 这是懒加载文件的源码, 可以看到, 文件默认执行的 window["webpackJsonp"]的push操作,
// 而push操作会触发webpackJsonpCallback函数,使依赖置为 0,并执行then方法
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  ["Lazy"],
  {
    // 一些注释
    "./src/Lazy.js": (function(module, exports) {

    eval("var __signature__ = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal[\"default\"].signature : function (a) {\n  return a;\n};\n\nconsole.log('This is a Lazy test');\n");

     }),
     ...,
     // 如果Lazy.js有import其他依赖, 则会出现在这里
   }
]);
=======================================================================================
// 然后再看看 webpackJsonpCallback的方法
function webpackJsonpCallback(data) {

  // 懒加载的文件: ['Lazy']
  var chunkIds = data[0]; 
  
  // 加载文件中依赖的文件, 结构大致如下
  // { 
  //	"./src/Lazy.js": (function() {}),
  //    "./src/xxx.js":  (function() {}),
  //      ...
  // }
  var moreModules = data[1]; 
  
  var moduleId, chunkId, i = 0, resolves = [];
  for(;i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
      // 在这里, 将promises中的resolve推入resolves中
      resolves.push(installedChunks[chunkId][0]);
    }
    // 将全局变量中的依赖置为0, 说明依赖已经安装完成
    installedChunks[chunkId] = 0;
  }
  // 遍历循环依赖,并将依赖执行代码 (function() {}) 推到全局变量module中
  // ∨∨∨ 后面__webpack_require__会用到 ∨∨∨
  for(moduleId in moreModules) {
    if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      modules[moduleId] = moreModules[moduleId];
    }
  }
  if(parentJsonpFunction) parentJsonpFunction(data);

  // 这里,开始执行promises中的resolve, 进入__webpack_require__的执行
  while(resolves.length) {
    resolves.shift()();
  }

};
复制代码
  • __webpack_require__源码
// import(/* webpackChunkName: 'Lazy' */ './Lazy').then(data => console.log(data))
// 实际上, import().then()在webpack的编译下会解释成以下这个样子
// 看上文 __webpack_require__.e = function requireEnsure(chunkId) 也就是动态创建标签,加载script
__webpack_require__.e(/*! import() | Lazy */ \"Lazy\")
  .then(
    // webpackJsonpCallback最后的resolve.shift()()将运行到这里   
    __webpack_require__.bind(null,\"./src/Lazy.js\")
  )
  .then(
    function (data) {\n        console.log(data);\n      }
  );

=======================================================================================
// 在这里出现了一个__webpack_require__, 我们看看这是个什么东东
function __webpack_require__(moduleId) {

  // 检查全局依赖中是否已经安装了,有的话则返回它的导出
  if(installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }
  // 如果没有,创建一个新的模块,并把它推到全局缓存module中
  var module = installedModules[moduleId] = {
    i: moduleId,
    l: false,
    exports: {},
    hot: hotCreateModule(moduleId),
    parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
    children: [],
    hot: hotCreateModule(moduleId),
    parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
    children: []
  };

  // 在全局变量中取出执行代码
  // 对应上面webpackJsonpCallback回调中,推入全局module缓存的代码块
  // ∨∨∨ 相当于执行 ∨∨∨
  //  (function(module, __webpack_exports__, __webpack_require__) {
  //    ...
  //  	console.log('xxx');
  //  }).call(module.exports, module, module.exports, __webpack_require__(moduleId))
  modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));

  module.l = true;

  // 返回模块的exports,给到then去执行
  // import('./Lazy').then(data => console.log(data))
  //                       ↑↑↑↑ 这个就是module.exports了
  return module.exports;
}
复制代码

webpack懒加载流程图

最后附上具体流程图 懒加载流程图

结语

至此, webpack的懒加载实现就讲完了。是不是感觉非常简单呢

后续结合懒加载的实现原理,可能会将出新的分享《自己手撸的热更新实现》

(如果有人看的话)

文章分类
前端
文章标签