好程序员web前端教程接下来降为大家继续分享js中的模块化知识 4.循环依赖 就是a依赖b,b依赖a,对于不同的规范也有不同的结果。 4.1CommonJS 对于node,每一个模块的exports={done:false}表示一个模块有没有加载完毕,经过一系列的加载最后全部都会变为true。 同步,从上到下,只输出已经执行的那部分代码 首先,我们写两个js用node跑一下: //a.js console.log('a.js') var b = require('./b.js') console.log(1) //b.js console.log('b.js') var a = require('./a.js') console.log(2)

//根据他的特点,require一个文件的时候,马上运行内部的代码,所以相当于 console.log('a.js') console.log('b.js') console.log(2) console.log(1) //输出是a.js、b.js、2、1 复制代码 加上export的时候: //a.js module.exports = {val:1} var b = require('./b.js') console.log(b.val) module.exports = {val:2} b.val = 3 console.log(b)

//b.js module.exports = {val:1} var a = require('./a.js') console.log(a.val) module.exports = {val:2} a.val = 3 console.log(a)

//1.在a.js暴露出去一个对象module.exports = {val:1} //2.require了b,来到b,运行b脚本 //3.b的第一行,把{val:1}暴露出去,引入刚刚a暴露的{val:1},打印a.val的结果肯定是1 //4.重新暴露一次,是{val:2},然后做了一件多余的事情,改a.val为3(反正是拷贝过的了怎么改都不会影响a.js),毫无疑问打印出{ val: 3 } //5.回到a,继续第三行,打印b.val,因为b暴露的值是2,打印2 //6.继续再做一件无意义的事情,打印{ val: 3 } 复制代码 解决办法:代码合理拆分 4.2ES6模块 ES6模块是输出值的引用,是动态引用,等到要用的时候才用,因此可以完美实现相互依赖,在相互依赖的a.mjs和b.mjs,执行a的时候,当发现import马上进入b并执行b的代码。当在b发现了a的时候,已经知道从a输入了接口来到b的,不会回到a。但是在使用的过程中需要注意,变量的顺序。 如果是单纯的暴露一个基本数据类型,当然会报错not defined。 因为函数声明会变量提升,所以我们可以改成函数声明(不能用函数表达式) //a.mjs import b from './b' console.log(b()) function a(){return 'a'} export default a //b.mjs import a from './a' console.log(a()) function b(){return 'b'} export default b 复制代码 4.3 require 我们一般使用的时候,都是依赖注入,如果是有循环依赖,那么可以直接利用require解决 define('a',['b'],function(b){ //dosomething }); define('b',['a'],function(a){ //dosomething }); //为了解决循环依赖,在循环依赖发生的时候,引入require: define('a',['b','require'],function(b,require){ //dosomething require('b') }); 复制代码 4.4 sea 循环依赖,一般就是这样 //a.js define(function(require, exports, module){ var b = require('./b.js'); //...... }); //b.js define(function(require, exports, module){ var a = require('./a.js'); //...... }); 复制代码 而实际上,并没有问题,因为sea自己解决了这个问题: 一个模块有几种状态: 'FETCHING': 模块正在下载中 'FETCHED': 模块已下载 'SAVED': 模块信息已保存 'READY': 模块的依赖项都已下载,等待编译 'COMPILING':模块正在编译中 'COMPILED': 模块已编译 步骤: 1.模块a下载并且下载完成FETCHED 2.编译a模块(执行回调函数) 3.遇到了依赖b,b和自身没有循环依赖,a变成SAVED 4.模块b下载并且下载完成FETCHED 5.b遇到了依赖a,a是SAVED,和自身有循环依赖,b变成READY,编译完成后变成COMPILED 6.继续回到a,执行剩下的代码,如果有其他依赖继续重复上面步骤,如果所有的依赖都是READY,a变成READY 7.继续编译,当a回调函数部分所有的代码运行完毕,a变成COMPILED 对于所有的模块相互依赖的通用的办法,将相互依赖的部分抽取出来,放在一个中间件,利用发布订阅模式解决 5.webpack是如何处理模块化的 假设我们定义两个js:app.js是主入口文件,a.js、b.js是app依赖文件,用的是COMMONJS规范 webpack首先会从入口模块app.js开始,根据引入方法require把所有的模块都读取,然后写在一个列表上: var modules = { './b.js': generated_b, './a.js': generated_a, './app.js': generated_app } 复制代码 'generated_'+name是一个IIFE,每个模块的源代码都在里面,不会暴露内部的变量。比如对于没有依赖其他模块的a.js一般是这样,没有变化: function generated_a(module, exports, webpack_require) { // ...a的全部代码 } 复制代码 对于app.js则不一样了: function generated_app(module, exports, webpack_require) { var a_imported_module = webpack_require('./a.js'); var b_imported_module = webpack_require('./b.js'); a_imported_module'inc'; b_imported_module'inc'; } 复制代码 webpack_require就是require、exports、import这些的具体实现,够动态地载入模块a、b,并且将结果返回给app 对于webpack_require,大概是这样的流程 var installedModules = {};//保存已经加载完成的模块 function webpack_require(moduleId) { if (installedModules[moduleId]) {//如果已经加载完成直接返回 return installedModules[moduleId].exports; } var module = installedModules[moduleId] = {//如果是第一次加载,则记录在表上 i: moduleId, l: false,//没有下载完成 exports: {} }; //在模块清单上面读取对应的路径所对应的文件,将模块函数的调用对象绑定为module.exports,并返回 modules[moduleId].call(module.exports, module, module.exports,webpack_require); module.l = true;//下载完成 return module.exports; } 复制代码 对于webpack打包后的文件,是一个庞大的IIFE,他的内容大概是这样子: (function(modules) { var installedModules = {}; function webpack_require(moduleId) { /.../} webpack_require.m = modules;//所有的文件依赖列表 webpack_require.c = installedModules;//已经下载完成的列表 webpack_require.d = function(exports, name, getter) {//定义模块对象的getter函数 if(!webpack_require.o(exports, name)) { Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: getter }); } }; webpack_require.n = function(module) {//当和ES6模块混用的时候的处理 var getter = module && module.__esModule ?//如果是ES6模块用module.default function getDefault() { return module['default']; } : function getModuleExports() { return module; };//是COMMONJS则继续用module webpack_require.d(getter, 'a', getter); return getter; }; webpack_require.o = function(object, property) { //判断是否有某种属性(如exports) return Object.prototype.hasOwnProperty.call(object, property); }; webpack_require.p = "";//默认路径为当前 return webpack_require(webpack_require.s = 0);//读取第一个模块 }) /************************************************************************/ //IIFE第二个括号部分 ([ (function(module, exports, webpack_require) { var a = webpack_require(1); var b = webpack_require(2); //模块app代码 }),

(function(module, exports, webpack_require) { //模块a代码 module.exports = ... }), (function(module, exports, webpack_require) { //模块b代码 module.exports = ... }) ]); 复制代码 如果是ES6模块,处理的方法也不一样。还是假设我们定义两个js:app.js是主入口文件,a.js、b.js是app依赖文件。 (function(modules) { //前面这段是一样的 }) ([ (function(module, webpack_exports, webpack_require) {//入口模块 Object.defineProperty(webpack_exports, "__esModule", { value: true }); var WEBPACK_IMPORTED_MODULE___0m = webpack_require(1); var WEBPACK_IMPORTED_MODULE_1__m = webpack_require(2); Object(WEBPACK_IMPORTED_MODULE___0m["a"])();//用object包裹着,使得其他模块export的内容即使是基本数据类型,也要让他变成一个引用类型 Object(WEBPACK_IMPORTED_MODULE_1__m["b"])();

}), (function(module, webpack_exports, webpack_require) { webpack_exports["a"] = a;//也就是export xxx //.... }), (function(module, webpack_exports, webpack_require) { webpack_exports["b"] = b; //.... }) ]);