目标:实现一个可以在浏览器跑通的commonJS。
理解commonJS:主要解决js没有模块化,没有原生的支持密闭作用域或依赖管理的问题;没有包管理系统,不能自动加载和安装依赖;**还有其他原因。
commonJS的工作流程:
1.路径分析
2.文件定位
3.编译执行
本案例直接将一个简单的js文件引用到代码中,并调用exports.方法。
我们来想想我们平时是怎么用commonJS的:???
1.首先通过require('./a.js')方法把a.js文件引用到自己的代码中,然后就可以正常使用a文件中通过module或者exports暴露出来的方法了。
???思考一下,一般引用一个方法都是通过(对象.方法)进行使用的,那require是怎样实现直接使用的呢??
2.那我们就顺着平时的使用进行commnJS的编写。
编写思路:
commonJS中最重要的方法就是require方法:整个流程就是:路径分析--文件定位--编译执行。整个案例主要关注的点就是编译执行。
我们将获取到的文件进行编译执行返回一个exports的引用。
1.创建模块,
2.执行文件内容,
3.放进缓存里,
4.返回exports
在这个过程中,首先我们要判断文件是不是已经在缓存里了,如果缓存里有直接从缓存里取出来,
下面是详细代码:
/** * 实现一个简单的 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 塞进文件模块所在的域里面
整个过程中需要了解的知识点:
1.闭包的使用:
/** * 拼一个闭包出来,IIFE * * @param {string} code 代码字符串 */ $wrap = (code) => { const wrapper = [ 'return (function (module, exports, require) {', '\n});' ]; return wrapper[0] + code + wrapper[1]; }
好处
①保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
②在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
③匿名自执行函数可以减少内存消耗
坏处
①其中一点上面已经有体现了,就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;
②其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响
2.new Function()的使用:
新建函数的语法:
let func = new Function ([arg1[, arg2[, ...argN]],] functionBody)
换句话说,函数的参数(或更确切地说,各参数的名称)首先出现,而函数体在最后。所有参数都写成字符串形式。
通过查看示例,可以更容易理解。这是一个有两个参数的函数:
let sum = new Function('a', 'b', 'return a + b');
alert( sum(1, 2) ); // 3
如果所要新建的函数没有参数,那么new Function()只有一个函数体参数:
let sayHi = new Function('alert("Hello")');
sayHi(); // Hello
3.沙箱环境的了解:
4.代理:Proxy:
5.Reflect的了解:
6.with的使用:
with 的写法
with (obj) { a = 3; b = 4; c = 5; }
在这段代码中,使用了 with 语句关联了 obj 对象,这就以为着在 with 代码块内部,每个变量首先被认为是一个局部变量,如果局部变量与 obj 对象的某个属性同名,则这个局部变量会指向 obj 对象属性。
7.IIFE:
IIFE: Immediately Invoked Function Expression,翻译过来就是立即调用的函数表达式。也就是说,在函数声明的同时立即调用这个函数。
IIFE函数声明和调用
(function foo(){ var a = 10; console.log(a); })();
首先注意一点的就是IIFE函数是由一对()将函数声明包裹起来的表达式。使得JS编译器不再认为这是一个函数声明,而是一个IIFE,即立刻执行函数表达式。
IIFE的优点
创建块级(私有)作用域,避免了向全局作用域中添加变量和函数,因此也避免了多人开发中全局变量和函数的命名冲突。
IIFE中定义的任何变量和函数,都会在执行结束时被销毁,这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
8.``的使用:
9.{${code}}的使用:
浙公网安备 33010602011771号