su37josephxia/frontend-interview

Day13 - 如何用闭包完成模块化(Webpack原理)

su37josephxia opened this issue · 21 comments

模块化最重要的有两点,1不能污染全局, 2.需要各自独立的作用域,互不干扰
我们可以将所有代码放入一个自执行函数中,避免污染全局,
并将除入口外其他模块的代码放入一个函数当中,并将所有模块函数放入一个对象当中
然后执行入口文件代码,分析依赖,这时需要一个查找依赖的函数,
这个函数的功能就是查找到依赖的模块函数并执行,拿到执行结果放入另一个对象中缓存以便下次使用

模块化具备两个必要条件:

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态

模块化的目的在于将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,模块之间通过接口调用。

  • 整体是自执行函数,闭包保证私有作用域,保证命名空间不冲突,都是独立的模块处理。

    (function(modules) {
    
    })([]);
    

    自执行函数的入参是个数组,这个数组包含了所有的模块

  • __webpack_require__(0) 函数,加载入口模块

  • installedModules缓存模块

    • 未被缓存,则加入缓存:installedModules[moduleId]
    • 已缓存,则直接返回模块:installedModules[moduleId].exports
  • 加载模块函数 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    • 模块函数内部再加载引用的模块(例如入口文件a.js中加载了一个b.js的模块)
  • 返回模块引用 return module.exports

如果想在所有的地方都能访问同一个变量,并且不使用全局变量,那么就可以使用模块化思维

  • 无论是require,还是ES6的modules,虽然实现方式不同,但是核心思路一样。
  • 第一:每一个单例就是一个模块。其实,你也知道,每一个文件也是一个模块。这里把每一个单例模式假想成一个单独的文件即可。定义一个模块,而变量名就是模块名。
  • var module_test = (function() {
    })();
  • 第二,每一个模块要想与其他模块交互,则必须有获取其它模块的能力,例如requirejs中的require与ES6modules中的import。//require
    var $ = require('jquery');
    //es6 modules
    import $ from 'jquery';
  • 第三,每一个模块都应该有对外的接口,以保证与其他模块交互的能力。这里直接使用return返回一个字面量对象的方式来对外提供接口。

回答(webpack 方向)

__webpack__modules__ 数组存放所有模块,每个模块是一个函数,至少需要一个 module 对象作为参数

__webpack_require__ 函数来模拟所有模块的调用,对于每一个模块,会有一个对应的 id 叫 moduleId,这个 moduleId 既是 __webpack_modules__ 的索引,也是 __webpack_require__ 缓存中的索引,都指向相同的模块,每个模块导出的是 module.export

webpack整体是一个自执行的函数,利用了闭包的特性,保证了内部变量私有化, 同时也不会对全局变量造成污染。

;(function (modules) {
  // 01 定义对象用于将来缓存被加载过的对象
  let installedModules = {}

  // 02 定义一个 __webpack_require__ 方法替换 import require 加载操作
  function __webpack_require__(moduleId) {
    // 2-1 判断当前缓存中是否存在要被加载的模块内容,如果存在直接返回
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports
    }

    // 2-2 如果当前缓存中不存在则需要我们自己定义{} 执行被导入的模块内容加载
    let module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    })

    // 2-3 调用当前 moduleId 对应的函数,然后完成内容的加载
    modules[moduleId].call(
      modules.exports,
      module,
      module.exports,
      __webpack_require__
    )

    // 2-4 当上述方法调用完成后,我们就可以修改 l 的值用于表示当前模块内容已经加载完成了
    module.l = true

    // 2-5 加载工作完成之后,要将拿回来的内容返回至调用的地方
    return module.exports
  }

  // 03 定义 m 属性 用于保存 modules
  __webpack_require__.m = modules

  // 04 定义 c 属性用于保存 cache
  __webpack_require__.c = installedModules

  // 05 定义 o 方法用于判断对象上是否存在指定的属性
  __webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty(object, property)
  }

  // 06 定义 d 方法用于在对象的身上添加指定的属性,同时给该属性提供一个 getter
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter })
    }
  }

  // 07 定义 r 方法用于标识当前模块是 es6 类型
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
    }
    Object.defineProperty(exports, '__esModule', { value: true })
  }

  // 08 定义 n 方法用于设置具体的 getter
  __webpack_require__.n = function (module) {
    let getter =
      module && module.__esModule
        ? function getDefault() {
            return module['default']
          }
        : function getModuleExports() {
            return module
          }
    __webpack_require__.d(getter, 'a', getter)
    return getter
  }

  // 11 定义 t 方法,用于加载指定 value 的模块内容,之后对内容进行处理再返回
  __webpack_require__.t = function (value, mode) {
    // 01 加载 value 对应的模块内容 ( value 一般就是模块 id)
    // 加载之后的内容又重新赋值给value变量
    if (mode & 1) {
      value = __webpack_require__(value)
    }

    if (mode & 8) {
      // 加载了可以直接返回使用的内容
      return value
    }

    if (mode & 4 && typeof value === 'object' && value && value.__esModule) {
      return value
    }

    // 如果 8 和 4 都没有成立则需要自定义 ns 来通过 default 属性返回内容
    let 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)
        )
      }
    }
  }

  // 09 定义 p 属性,用于保存资源访问路径
  __webpack_require__.p = ''

  // 10 调用 __webpack_require__ 方法执行模块导入与加载操作
  return __webpack_require__((__webpack_require__.s = './src/index.js'))
})({
  './src/c.js': function (module, __webpack_exports__, __webpack_require__) {
    'use strict'
    __webpack_require__.r(__webpack_exports__)
    __webpack_require__.d(__webpack_exports__, 'age', function () {
      return age
    })
    __webpack_exports__['default'] = 'aaaaaa'
    const age = 90
  },

  './src/index.js': function (
    module,
    __webpack_exports__,
    __webpack_require__
  ) {
    'use strict'
    __webpack_require__.r(__webpack_exports__)
    var _c_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
      /*! ./c.js */ './src/c.js'
    )
    console.log('index.js')
    console.log(
      _c_js__WEBPACK_IMPORTED_MODULE_0__['default'],
      '----',
      _c_js__WEBPACK_IMPORTED_MODULE_0__['age']
    )
  }
})

具体流程,当有模块数组作为参数传入立即执行函数时

  • 首先会定义installedModules变量用来缓存已加载的模块;
  • 定义 __webpack_require__ 函数,参数为模块Id,返回被加载模块中导出的内容;
    • __webpack_require__中,首先会检查是否在缓存中,如果已在则直接返回缓存模块的exports
    • 没有缓存,则首先初始化模块,并进行缓存。
    • 然后调用执行模块,将modulemodule.exports__webpack_require__作为参数传入,将this指向绑定到 module.exports,确保模块中的this指向当前模块。
    • 调用完成后,模块标记为已加载
    • 返回模块中的exports内容。
  • 利用定义好的 __webpack_require__函数,引入第0个模块也就是入口模块。

模块化,就是能够实现某一功能的代码,封装成一个函数,只暴露出一个接口来供外部函数调用;被封装好的各模块之间的作用域互不冲突;我们可以通过闭包来实现模块的封装

必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
封闭函数必须返回至少一个内部函数(对象字面量,{key:value...} ),这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
webpack的运行时代码,其中就是关于模块的实现,并且会发现这是个自执行函数
自执行函数体里的逻辑就是处理模块的逻辑。关键在于 webpack_require 函数,这个函数就是 require 或者是 import 的替代

主要的思路是创建一个临时的函数作用域,会在初始化执行完后销毁,不污染全局作用域。

  1. 使用iife即时函数,通过传参传入一个对象,这个对象包含了每一个文件的代码字符串和路径名的映射。

  2. 生成一个 require 函数,用于给入口 require('index.js') 使用。这个 require 函数内部先声明一个 exports 对象,然后通过一个 iife 即时函数,传参 require,exports,在封闭的函数作用域内使用 evel 执行对应模块的代码字符串,最后 require 函数会返回内部声明的 exports 对象模拟导出。这里闭包了 require 和 exports ,而且是一个递归调用。eval作用域拥有 require,exports,可以实现模块内部的引入和导出。实现我们在js文件内写的 require 和 modules.exports。这也是为什么说 require 的引入是把整个文件执行一遍。

  3. 最后,因为 require 函数是在即时函数内的,所以当即时函数执行完后不会留下 require 的踪影,实现了模块化,不污染全局变量。

模块化封装的过程中使用了闭包。示例代码如下:

function module(){
  const snail = {
    name:'snail',
    age:18,
    code:() => {console.log('I am codeing')},
    say:() => {console.log('hello world!')}
  }
  return {
    getAge:() => snail.age,
    code:() => {snail.code()},
    say:() => {snail.say()}
  }
}

模块化具备两个必要条件:

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态
    模块化的目的在于将一个复杂的功能拆分为多个子功能,形成相互独立的模块,最后通过有效的组合,实现复杂功能。

模块化就是对某种特定功能的封装,然后在主模块中引入各个模块,从而组建成完整的体系。
为了保证各个模块间环境相互独立,所以我们需要用即时函数将各个模块的代码包裹起来,这样可以有效地防止模块内的变量或函数污染到全局或各个模块间相互影响。
webpack的原理正是于此,通过即时函数封装起require(导入模块)和exports(导出模块)的功能,功能内部定义的属性不会互相影响也不会污染到全局中。

模块化通过自执行函数和闭包形成。目的是为了每个模块互相独立,不污染全局作用域。
webpack打包形成的文件在一个js中,该js整体是一个自执行函数,不会污染全局变量。被导入的js通过模拟require和exports函数形成局部变量。主入口模块是一个自执行函数,函数内部引用上面的require和exports对象形成闭包,

库文件首先要保证不能污染全局

打包后最基础版本就是下面这样

(function (modules) {/* 省略函数内容 */})
([
function (module, exports, __webpack_require__) {
    /* 模块index.js的代码 */
},
function (module, exports, __webpack_require__) {
    /* 模块bar.js的代码 */
}
]);

可以看到,整个打包生成的代码是一个IIFE(立即执行函数)

webpack也控制了模块的module、exports和require

// 1、模块缓存对象
var installedModules = {};
// 2、webpack实现的require
function __webpack_require__(moduleId) {
    // 3、判断是否已缓存模块
    if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }
    // 4、缓存模块
    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
    };
    // 5、调用模块函数
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 6、标记模块为已加载
    module.l = true;
    // 7、返回module.exports
    return module.exports;
}
// 8、require第一个模块
return __webpack_require__(__webpack_require__.s = 0);

模块数组作为参数传入IIFE函数后,IIFE做了一些初始化工作:

  1. IIFE首先定义了installedModules ,这个变量被用来缓存已加载的模块。
  2. 定义了__webpack_require__ 这个函数,函数参数为模块的id。这个函数用来实现模块的require。
  3. webpack_require 函数首先会检查是否缓存了已加载的模块,如果有则直接返回缓存模块的exports。
  4. 如果没有缓存,也就是第一次加载,则首先初始化模块,并将模块进行缓存。
  5. 然后调用模块函数,也就是前面webpack对我们的模块的包装函数,将module、module.exports和__webpack_require__作为参数传入。注意这里做了一个动态绑定,将模块函数的调用对象绑定为module.exports,这是为了保证在模块中的this指向当前模块。
  6. 调用完成后,模块标记为已加载。
  7. 返回模块exports的内容。
  8. 利用前面定义的__webpack_require__ 函数,require第0个模块,也就是入口模块。

require入口模块时,入口模块会收到收到三个参数

function(module, exports, __webpack_require__) {
    "use strict";
    var bar = __webpack_require__(1);
    bar.bar();
}

webpack传入的第一个参数module是当前缓存的模块,包含当前模块的信息和exports;第二个参数exports是module.exports的引用,这也符合commonjs的规范;第三个__webpack_require__ 则是require的实现。

在我们的模块中,就可以对外使用module.exports或exports进行导出,使用__webpack_require__导入需要的模块,代码跟commonjs完全一样。

这样,就完成了对第一个模块的require,然后第一个模块会根据自己对其他模块的require,依次加载其他模块,最终形成一个依赖网状结构。webpack管理着这些模块的缓存,如果一个模块被require多次,那么只会有一次加载过程,而返回的是缓存的内容,这也是commonjs的规范。

原理还是很简单的,其实就是实现exports和require,然后自动加载入口模块,控制缓存模块

必须有外部的封闭自调函数函数iife
封闭函数必须至少返回一个内部函数
这样内部函数才能在私有作用域中形成闭包

模块化原理构建一个自执行的函数,这里就利用了闭包的特性,保证了内部变量私有化, 同时也不会对全局变量造成污染。
同时我们在这个自执行函数中设置一个对象来模拟并保存模块中exports的内容。同时声明一个函数来模拟 require方法。
每一个模块的执行也都放到一个自执行函数中。

webpack中是如何使用闭包完成模块化呢?

  • 整体使用自执行函数包裹编译结果,保证任何变量不会泄露,污染全局
(() => {

})();
  • 并用变量来保存模块集合
var o = {
  // 原来的a.js
  85: (o) => {
    const r = Date.now();
    o.exports = "A:" + r;
  },
},
  • 模拟exports、require
-r = {}; // 模拟exports对象
function t(e) {  // 模拟require函数
  var n = r[e];
  if (void 0 !== n) return n.exports;
  var s = (r[e] = { exports: {} });
  return o[e](s, s.exports, t), s.exports;
}

webpack如何利用闭包完成模块化

  • 使用即时函数分割作用域,避免全局变量污染
  • 闭包保存依赖图
  • 模拟exports和require
function bundle(graph) {
  let modules = "";
  // 源代码需要export require module
  graph.forEach((module) => {
    module += `${module.id}: [
	function(require, module, exports) {
		${module.code}
	},
	${JSON.stringify(module.mapping)},
]`;
  });
  const result = `
		(function(modules) {
			funciton require(id) {
				const [fn, mapping] = modules[id] 

				function localRequire(relativePath) {
					return require(mapping[relativePath])
				}

				const module = { exports: {} }
				fn(localRequire, module, module.exports)

				return module.exports
			}
			require(0) 
		})({${modules}})
	`;

  return result;
}

webpack如何利用闭包完成模块化?

  1. webpack打包后的整体其实就是一个立即执行函数,函数体内的每个模块也是用立即执行函数包裹,这样wepack打包函数不会污染外部环境,每个打包模块之间也不互相干扰;
  2. 在函数内部定义了私有属性,通过闭包将多个模块的方法和属性封装起来,引用作为全局变量的私有属性为其赋值,这样就将多个独立模块集中到一个私有属性上管理;
  3. 外部环境引入包的时候就要传入路径参数,这个路径参数就是wepack暴露给使用者的对象的键,对应的值就是解析出的对应模块的函数方法封装体。

webpack原理是将不同的模块打包成不同的集合
然后模拟 export 和 require 方法
并将主模块打包成一个立即执行函数,在立即执行函数中通过模拟的 export 和 require 方法将不同的模块导入到当前函数中,此处应用了闭包的原理,防止模块中的变量暴露污染全局变量

webpack模块化其实是通过自执行函数启动,其中的模块文件也是一个个的自执行函数,利用闭包的特性,将各模块的变量和方法与外部隔离开,并自定义exports 和require 方法,将内部的方法或变量导出并记录映射关系,在外部就可以通过一定的id去获取对应的方法或变量

如何用闭包完成模块化
模块化通过自执行函数和闭包形成。目的是为了每个模块互相独立,不污染全局作用域。
为了保证各个模块间环境相互独立,要用即时函数将各个模块的代码包裹起来,这样可以有效地防止模块内的变量或函数污染到全局或各个模块间相互影响。模拟exports和require方法,自动加载入口模块,控制缓存模块