当前位置: 首页 > 科技观察

阮一峰:JavaScript模块的循环加载

时间:2023-03-21 12:57:46 科技观察

“循环依赖”是指脚本a的执行依赖于脚本b,脚本b的执行依赖于脚本a。//a.jsvarb=require('b');//b.jsvara=require('a');通常,“循环加载”意味着存在强耦合。如果处理不当,还可能导致递归加载,使程序无法执行,因此应该避免。但实际上,这是很难避免的,尤其是对于依赖关系复杂的大型项目。很容易让a依赖b,b依赖c,c依赖a。这意味着模块加载机制必须考虑“循环加载”的情况。本文介绍了JavaScript语言如何处理“循环加载”。目前最常见的两种模块格式,CommonJS和ES6,处理方式不同,返回结果也不同。CommonJS模块CommonJS模块的重要特性是加载时执行,即当需要脚本代码时,它会完全执行。因此,CommonJS规定,一旦发现某个模块被“循环加载”,就会立即停止加载,只输出执行过的部分。我们看一下官方文档中的例子。脚本文件a.js代码如下。exports.done=false;varb=require('./b.js');console.log('ina.js,b.done=%j',b.done);exports.done=true;console.log('a.js执行完成');上面代码中,a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意此时a.js代码就停在这里了,等待b.js执行完毕再继续。再看看b.js的代码。exports.done=false;vara=require('./a.js');console.log('inb.js,a.done=%j',a.done);exports.done=true;console.log('b.js执行完成');上面代码中,b.js执行到第二行时,会加载a.js。这时候,“循环加载”就发生了。为了避免无限递归,执行引擎不会再执行a.js,只返回执行过的部分。a.js中已经执行的部分只有一行。exports.done=false;因此,对于b.js来说,它只输入一个donefroma.js的值为false的变量。然后,b.js继续执行,当所有执行完成后,将执行权交还给a.js。所以a.js会继续执行,直到执行完毕。我们写一个脚本main.js来验证这个过程。vara=require('./a.js');varb=require('./b.js');console.log('在main.js中,a.done=%j,b.done=%j',a.done,b.done);执行main.js,运行结果如下。$nodemain.js在b.js中,a.done=falseb.js在a.js中执行,b.done=truea.js在main.js中执行,a.done=true,b.done=true的上面的代码证明了两件事。一种是,在b.js中,a.js没有执行,只执行了***行。第二,当main.js执行到第二行时,不再执行b.js,而是输出b.js缓存的执行结果,也就是它的第四行。exports.done=true;ES6模块ES6模块的运行机制与CommonJS不同。当遇到模块加载命令import时,不会执行模块,只会生成引用。当你真正需要使用它时,去模块中获取值。因此,ES6模块是动态引用,不存在缓存值的问题,模块中的变量绑定到所在模块。请看下面的例子。//m1.jsexportvarfoo='bar';setTimeout(()=>foo='baz',500);//m2.jsimport{foo}from'./m1.js';console.log(foo);setTimeout(()=>console.log(foo),500);上面代码中,m1.js的变量foo刚加载时等于bar,500毫秒后变为等于baz。让我们看看m2.js是否可以正确读取此更改。$babel-nodem2.jsbarbaz上面的代码表明,ES6模块不缓存运行结果,而是动态获取加载模块的值,变量始终绑定到所在模块。这导致ES6处理“循环加载”的方式与CommonJS根本不同。ES6根本不检查是否发生了“循环加载”,而只是生成对加载模块的引用。需要开发者保证在真正获取到值的时候能够获取到该值。请参见下面的示例(取自《Exploring ES6》,作者AxelRauschmayer博士)。//a.jsimport{bar}from'./b.js';exportfunctionfoo(){bar();console.log('执行完成');}foo();//b.jsimport{foo}from'./a.js';exportfunctionbar(){if(Math.random()>0.5){foo();}}根据CommonJS规范,上述代码无法执行。a先加载b,然后b再次加载a。此时a没有任何执行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,下面的foo()就会报错。但是,ES6可以执行上面的代码。之所以在$babel-nodea.js执行完后可以执行a.js,是因为ES6加载的变量都是动态引用所在模块的。只要引用存在,代码就会执行。我们来看一个ES6模块加载器SystemJS给出的例子。//even.jsimport{odd}from'./odd'exportvarcounter=0;exportfunctioneven(n){counter++;returnn==0||odd(n-1);}//odd.jsimport{even}from'。/even';exportfunctionodd(n){returnn!=0&&even(n-1);}在上面的代码中,even.js中的函数foo有一个参数n,只要不等于0,就会减去1并将其传入以加载Theodd()。odd.js做了类似的事情。运行上面的代码,结果如下。$babel-node>import*asmfrom'./even.js';>m.even(10);true>m.counter6>m.even(20)true>m.counter17上面代码中,参数n变了从10为0的过程中,foo()一共会执行6次,所以变量counter等于6。第二次调用even()时,参数n由20变为0,foo()一共会执行11次,加上之前的6次,所以变量counter等于17。如果这个例子改写成CommonJS,根本无法执行,会报错。//even.jsvarodd=require('./odd');varcounter=0;exports.counter=counter;exports.even=function(n){counter++;return==0||odd(n-1);}//odd.jsvareven=require('./even');exports.odd=function(n){returnn!=0&&even(n-1);}上面代码中,even.js加载odd.js,odd.js又去加载even.js,形成“循环加载”。这时执行引擎会输出even.js已经执行完的部分(没有结果),所以在odd.js中,变量even等于null,当even(n-1)稍后调用。$node>varm=require('./even');>m.even(10)类型错误:odddisnotafunction(结束)