波比-JS模块化详解

1.什么是模块化

模块化是一种代码管理、组织和通信的模式

2.为什么要用模块化?

随着项目复杂度的提高,项目变得越来越难以维护,js模块化也油然而生,所有的模块都处于全局域下,容易造成命名冲突的问题,而且依赖关系不明显。引入js模块化的作用是:

  1. 为了将复杂的问题分解成更小的子问题(关注点分离)
  2. 更好地进行代码管理,方便代码替换、复用、拓展
  3. 方便多人协同开发

3.模块化发展

  1. 最开始模块化混沌时期,靠经验组织代码
  2. 使用闭包
  3. 当前阶段,CommonJS、ES6

4.循环引用

当a模块引用了b模块,b模块又引用了a模块,两个模块怎么执行?

a.js

exports.a = false
const b = require('./b.js')

console.log('a 模块, 输出了b模块的内容为:', b);
console.log('a 模块执行到这就结束了');

b.js

exports.b = false
const a = require('./a.js')

console.log('b 模块, 输出了b模块的内容为:', a);
console.log('b 模块执行到这就结束了');

main.js

const a = require('./a.js')
const b = require('./b.js') // 此中的b模块使用的是缓存中的内容
console.log('执行 main, 输出', a, b)

执行main.js结果

在这里插入图片描述
CommonJS中代码是同步执行的,当a模块执行到引入b模块时,控制权会交给b模块,在b模块中执行到引入a模块时,并不会将控制器跳转至a模块,只有当在b模块执行完之后才会将控制权跳转回a模块,b模块中引入的a只包含了a模块中引入b模块之前的内容。

5.相关问题

  1. 什么是闭包
    闭包就是能读取一个函数内部变量的函数。我们知道一个函数可以访问它外面的全局变量,而外面却不能访问函数内的内部变量,如果我们要能读取函数内的变量,就可以在这个函数outer里面再定义一个函数inner用来访问outer的内部变量,然后将inner作为outer的返回值,这样就是一个闭包

  2. 闭包中变量是存在栈内存还是堆内存?
    堆内存,js中全局变量在页面关闭时被销毁,局部变量在函数执行完毕被销毁,由于闭包形式内部函数不算执行完毕,因为内部函数还保持着对其外部函数变量的引用,所以这个变量会一直保存在内存之中

  3. CommonJS引用是值拷贝还是引用拷贝?ES6引用是值拷贝还是引用拷贝?
    CommonJS引用是值拷贝,对引用的变量进行修改不会影响到其他地方的引用,ES6引用是拷贝

6.简易实现一个commonJS

/**
 * 实现一个简单的 commonjs 模块加载器,偏浏览器端的实现
 * 
 * 指导准则:COMMONJS 规范 -- 火狐的一个工程师
 * 
 * 2 个部分:
 * 
 * 1、模块加载器:解析文件地址,有一个寻找的规则,目的肯定就是找到文件
 * 2、模块解析器:执行文件内容的,Node 里面是使用了 v8 执行的
 */

class Module {
  constructor(moduleName, source) {
    // 暴露数据
    this.exports = {};
    // 保存一下模块的信息
    this.moduleName = moduleName;
    // 缓存
    this.$cacheModule = new Map();
    // 源代码
    this.$source = source;
  }

  /**
   * require
   * 
   * useage: require('./a.js')
   * 
   * @param {string} moduleName 模块的名称,其实就是路径信息
   * @param {string} source 文件的源代码,因为省略了加载器部分的实现,所以这里直接传入文件源代码
   * 
   * @return {object} require 返回的结果就是 exports 的引用
   */
  require = (moduleName, source) => {
    // 每一次 require 都执行文件内容的话,开销太大,所以加缓存
    if (this.$cacheModule.has(moduleName)) {
      // 注意,返回的是 exports
      return this.$cacheModule.get(moduleName).exports;
    }

    // 创建模块
    const module = new Module(moduleName, source);

    // 执行文件内容
    const exports = this.compile(module, source);

    // 放进缓存
    this.$cacheModule.set(moduleName, module);

    // 返回 exports
    return exports;
  }

  /**
   * // a.js
   * const b = require('./b.js');
   * 
   * b.action();
   * 
   * exports.action = function() {};
   * 
   * // b.js
   * const a = require('./a.js');
   * 
   * exports.action = function() {};
   */

  /**
   * 拼一个闭包出来,IIFE
   * 
   * @param {string} code 代码字符串
   */
  $wrap = (code) => {
    const wrapper = [
      'return (function (module, exports, require) {',
      '\n});'
    ];

    return wrapper[0] + code + wrapper[1];
  }

  /**
   * 简单实现一个能在浏览器跑的解释器 vm.runInThisContext
   * 核心的点是要创建一个隔离的沙箱环境,来执行我们的代码字符串
   * 
   * 隔离:不能访问闭包的变量 1,不能访问全局的变量 3,只能访问我们传入的变量 2
   * 
   * eval: 可以访问全局/闭包,但是需要解释执行,ES5 之后,如果是间接使用 eval
   *       -> (0, eval)('var a = b + 1'); ❌
   * new Function: 不可以访问闭包,可以访问全局,只编译一次 1 ✅
   * with: with 包裹的对象,会被放到原型链的顶部,而且底层是通过 in 操作符判断的 🤔
   *       如果通过 with 塞入我们传入的数据 2 ✅
   *       不管是啥属性,都从我们塞入的对象取值,取不到就返回 undefined,这样就永远不会访问全局的域了 3 ✅
   * 
   * unscopable: 这个对象是不能够被 with 处理的
   * @param {string} code 代码字符串
   */
  $runInThisContext = (code, whiteList=['console']) => {
    // 使用 with 保证可以通过我们传入的 sandbox 对象取数据
    // new Function 不能访问闭包
    const func = new Function('sandbox', `with(sandbox) {${code}}`);
    return function(sandbox) { // 👈 塞到文件源代码中的变量
      if (!sandbox || typeof sandbox !== 'object') {
        throw Error('sandbox parameter must be an object.');
      }

      // 代理
      const proxiedObject = new Proxy(sandbox, {
        // 专门处理 in 操作符的
        has(target, key) {
          if (!whiteList.includes(key)) {
            return true;
          }
        },
        get(target, key, receiver) {
          if (key === Symbol.unscopables) {
            return void 0;
          }
          return Reflect.get(target, key, receiver);
        }
      });

      return func(proxiedObject);
    }
  }


  /**
   * 执行文件内容,入参数是文件源代码字符串
   * 
   * IIFE: (function() {})(xxx, yyy);
   * 
   * function (proxiedSandbox) {
   *   with (proxiedSandbox) {
   *      return (function (module, exports, require) {
   *        // 文件内容字符串
   *      })
   *   }
   * }
   */
  compile = (module, source) => {
    // return (function(module, exports, require) { //xxxx }); ⚠️
    const iifeString = this.$wrap(source);
    // 创建沙箱的执行环境
    const compiler = this.$runInThisContext(iifeString)({});
    // (function(){ //xxx }) + ();
    // compiler + ();
    // -> compiler(); === compiler.call()
    compiler.call(module, module, module.exports, this.require);

    return module.exports;
  }
}

/**
 * demo 验证
 */
const m = new Module();

// a.js
const sourceCodeFromAModule = `
  const b = require('b.js', 'const a = require("a.js"); console.log("a module: ", a); exports.action = function() { console.log("execute action from B module successfully 🎉") }');

  b.action();

  exports.action = function() {
    console.log("execute action from A module!");
  }
`
m.require('a.js', sourceCodeFromAModule);
 
// require -> 【1、模块加载(获取文件字符串)2、解释执行字符串 3、exports 4、缓存】
// IIFE 的方式把 require 塞进文件模块所在的域里面
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值