JavaScript循环加载模块的方法及模块加载技术思考

"循环加载"(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

    // a.js
    var b = require('b');

    // b.js
    var a = require('a');

通常,"循环加载"表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。

但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖b,b依赖c,c又依赖a这样的情况。这意味着,模块加载机制必须考虑"循环加载"的情况。

本文介绍JavaScript语言如何处理"循环加载"。目前,最常见的两种模块格式CommonJS和ES6,处理方法是不一样的,返回的结果也不一样。

一、CommonJS模块的加载原理

介绍ES6如何处理"循环加载"之前,先介绍目前最流行的CommonJS模块格式的加载原理。

CommonJS的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

    {
      id: '...',
      exports: { ... },
      loaded: true,
      ...
    }

上面代码中,该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。

以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。

二、CommonJS模块的循环加载

CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。CommonJS的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

让我们来看,官方文档里面的例子。脚本文件a.js代码如下。

    exports.done = false;
    var b = require('./b.js');
    console.log('在 a.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;
    var a = require('./a.js');
    console.log('在 b.js 之中,a.done = %j', a.done);
    exports.done = true;
    console.log('b.js 执行完毕');

上面代码之中,b.js执行到第二行,就会去加载a.js,这时,就发生了"循环加载"。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。

a.js已经执行的部分,只有一行。

    exports.done = false;

因此,对于b.js来说,它从a.js只输入一个变量done,值为false。

然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。我们写一个脚本main.js,验证这个过程。

    var a = require('./a.js');
    var b = require('./b.js');
    console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

执行main.js,运行结果如下。

    $ node main.js

    在 b.js 之中,a.done = false
    b.js 执行完毕
    在 a.js 之中,b.done = true
    a.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.js
    export var foo = 'bar';
    setTimeout(() => foo = 'baz', 500);

    // m2.js
    import {foo} from './m1.js';
    console.log(foo);
    setTimeout(() => console.log(foo), 500);

上面代码中,m1.js的变量foo,在刚加载时等于bar,过了500毫秒,又变为等于baz。

让我们看看,m2.js能否正确读取这个变化。

    $ babel-node m2.js

    bar
    baz

上面代码表明,ES6模块不会缓存运行结果,而是动态地去被加载的模块取值,以及变量总是绑定其所在的模块。

这导致ES6处理"循环加载"与CommonJS有本质的不同。ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

请看下面的例子(摘自 Dr. Axel Rauschmayer 的《Exploring ES6》)。

    // a.js
    import {bar} from './b.js';
    export function foo() {
      bar();  
      console.log('执行完毕');
    }
    foo();

    // b.js
    import {foo} from './a.js';
    export function bar() {  
      if (Math.random() > 0.5) {
        foo();
      }
    }

按照CommonJS规范,上面的代码是没法执行的。a先加载b,然后b又加载a,这时a还没有任何执行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。

但是,ES6可以执行上面的代码。

    $ babel-node a.js

    执行完毕

a.js之所以能够执行,原因就在于ES6加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。

我们再来看ES6模块加载器SystemJS给出的一个例子。

    // even.js
    import { odd } from './odd'
    export var counter = 0;
    export function even(n) {
      counter++;
      return n == 0 || odd(n - 1);
    }

    // odd.js
    import { even } from './even';
    export function odd(n) {
      return n != 0 && even(n - 1);
    }

上面代码中,even.js里面的函数foo有一个参数n,只要不等于0,就会减去1,传入加载的odd()。odd.js也会做类似操作。

运行上面这段代码,结果如下。

    $ babel-node
    > import * as m from './even.js';
    > m.even(10);
    true
    > m.counter
    6
    > m.even(20)
    true
    > m.counter
    17

上面代码中,参数n从10变为0的过程中,foo()一共会执行6次,所以变量counter等于6。第二次调用even()时,参数n从20变为0,foo()一共会执行11次,加上前面的6次,所以变量counter等于17。

这个例子要是改写成CommonJS,就根本无法执行,会报错。

    // even.js
    var odd = require('./odd');
    var counter = 0;
    exports.counter = counter;
    exports.even = function(n) {
      counter++;
      return n == 0 || odd(n - 1);
    }

    // odd.js
    var even = require('./even').even;
    module.exports = function(n) {
      return n != 0 && even(n - 1);
    }

上面代码中,even.js加载odd.js,而odd.js又去加载even.js,形成"循环加载"。这时,执行引擎就会输出even.js已经执行的部分(不存在任何结果),所以在odd.js之中,变量even等于null,等到后面调用even(n-1)就会报错。

    $ node
    > var m = require('./even');
    > m.even(10)
    TypeError: even is not a function

    

关于javascript模块加载技术的一些思考

前不久有个网友问我在前端使用requireJs和seajs的问题,我当时问他你们公司以前有没有自己编写的javascript库,或者javascript框架,他的回答是什么都没有,他只是听说像requirejs和seajs是新东西新技术,很有价值所以想用它。

这位网友的问题引起了我对javascript模块加载技术的思考,上篇文章我给出了自己写的一个javascript库的基本结构,其实写这篇文章的一个起因就是因为我想使用requirejs或者seajs这样的技术来重新设计我写javascript库的基本模型,当我深入了解这个技术后,我发现使用模块加载系统来解决把javascript库里通用代码和业务代码解耦的问题,是不正确的,模块加载系统的作用范围是解决不同javascript库之间的依赖问题,而不是帮助你去如何开发一个javascript库。

那么什么是javascript的模块加载系统呢?

模块系统主要为了解决不同javascript库里操作对象的命名冲突问题以及不同javascript库之间依赖的问题,模块加载系统是针对大型web前端应用或者说是巨型的web前端应用。

一般巨型的web前端应用页面里,该页面的功能非常丰富,业务非常庞杂,而且随着时间推移,页面的功能经常会发生变迁,所以导致前端开发人员经常要开发出针对新功能的功能模块,但是实际业务里各个功能模块之间的功能还有可能相互渗透,相互依赖的,关系错综复杂,当页面复杂后,各个前端库之间的关系就出现很难管理和控制的问题,这个时候模块加载系统才会派上用场。

对于大多数程序员而言,能独立承担这么大web前端应用的机会并不是太多,而开发中小型web前端应用的机会会多的多,例如企业级的web项目,这样的项目使用到的javascript库的种类很少,各个库的依赖关系很好控制,是没有必要引入什么模块管理系统的必要,就算很多中小型互联网公司的网页,估计也不会比企业级web应用前端那么复杂,所以它的模块之间或者说javascript库之间的关系很好管理的。其实像上面这些中小应用都是针对某些或某一个具体场景进行的,因此我个人觉得面对这样的web前端项目我们最后能自己形成一个独立的javascript库,这个库的特点应该和jQuery这种类型的库类似:一个主库加上若干个插件库的模式,主库的目的是解决通用性的问题,它应该是可以进行复用和迁移的,而插件库的目的往往和业务代码相关的,不过为了区别主库和插件库的作用域问题,所以我在库里加上了命名空间的功能。

Javascript模块加载技术和hadoop的技术有些相同点,那就是它们都是针对超大型系统的技术,它们只有在一定条件下才能发挥它们的作用,所以这些技术都是从大型互联网公司推出出来,因为大型互联网公司随着应用变大变复杂后必须要去解决的问题,当你系统还是处于起步阶段,这些技术的运用往往要谨慎,我们应该找出最简单最有效的方法解决我们实际问题,如果你觉得这个系统以后会越来越大,那么你应该保留以后使用这些技术的接口,如果使用太早了,很有可能当系统规模扩大后,你重构代码的代价会更高。

对于模块加载系统,它最适合的场景是解决大型web前端应用模块之间的解耦的问题,如果我们只要新写一个javascript文件就马上使用模块加载技术,这个不是有点滥用技术的嫌疑了,我们运用某个技术之前不应该只是考虑它怎么用,如何用,应该还要想想使用它有没有价值的问题。

最后我想说的是,我觉得中小型web前端应用到了生产部署,因为javascript并非最复杂,所以所有外部javascript文件都打包成一个javascript外部文件最好,这样的好处就是减少了http请求个数,使用模块加载技术会让你打包文件操作很麻烦,甚至无法做到(像requirejs和seajs的模块都是以文件为单位的,每个模块就是一个独立文件),这和解决减少http目的是相悖的。

时间: 2024-10-06 11:12:10

JavaScript循环加载模块的方法及模块加载技术思考的相关文章

WinForm ListView 大数据提高加载速度的方法 虚拟模式加载

将VirtualMode 属性设置为 true 会将 ListView 置于虚拟模式.控件不再使用Collection.Add()这种方式来添加数据,取而代之的是使用RetrieveVirtualItem(Occurs when the ListView is in virtual mode and requires a ListViewItem.)和CacheVirtualItems两个事件,单独使用RetrieveVirtualItem也可以,CacheVirtualItems这个事件主要是

《JavaScript框架设计》——第 2 章  模块加载系统 2.1AMD规范

第 2 章 模块加载系统 任何语言一到大规模应用阶段,必然要经历拆分模块的过程,以有利于维护与团队协作.与Java走得最近的dojo率先引入了加载器,早期的加载器都是同步的,使用document.write与同步Ajax请求实现.后来dojo开始以JSONP的方法设计它的每个模块的结构,以script节点为主体加载它的模块,这个就是目前主流的加载器方式.不得不提的是,dojo的加载器与AMD规范的发明者都是James Burke,dojo加载器独立出来就是著名的require加载器. 2.1 A

《JavaScript框架设计》——第 2 章  模块加载系统2.1 AMD规范

第 2 章 模块加载系统 任何语言一到大规模应用阶段,必然要经历拆分模块的过程,以有利于维护与团队协作.与Java走得最近的dojo率先引入了加载器,早期的加载器都是同步的,使用document.write与同步Ajax请求实现.后来dojo开始以JSONP的方法设计它的每个模块的结构,以script节点为主体加载它的模块,这个就是目前主流的加载器方式.不得不提的是,dojo的加载器与AMD规范的发明者都是James Burke,dojo加载器独立出来就是著名的require加载器.本章将为你深

javascript顺序加载图片的方法_javascript技巧

本文实例讲述了javascript顺序加载图片的方法.分享给大家供大家参考.具体如下: javascript监听一个图片是否加载完毕 如果加载完成再加载下一张,不是一次性从服务器加载 减少服务器压力, 可用到的地方:比如制作类似google地图的应用,可以使小图一张一张的加载 function Load_pic(arr){ this.loop_f=function(i,o_file,len,f,obj){ if(i<len-1){ i=i+1; f(i,o_file,len,obj); } };

javascript判断图片是否加载完成的方法推荐_javascript技巧

load事件 <script type="text/javascript"> $('img').onload = function() { //code } </script> 优点:简单易用,不影响HTML代码. 缺点:只能指定一个元素,javascipt代码必须置于图片元素的下方 jquery方法 <script type="text/javascript"> $(function(){ $('.pic1').each(fun

Javascript实现图片懒加载插件的方法_javascript技巧

前言 网络上各大论坛,尤其是一些图片类型的网站上,在图片加载时均采用了一种名为懒加载的方式,具体表现为,当页面被请求时,只加载可视区域的图片,其它部分的图片则不加载,只有这些图片出现在可视区域时才会动态加载这些图片,从而节约了网络带宽和提高了初次加载的速度,具体实现的技术并不复杂,下面分别对其说明. Web 图片的懒加载就是通过读取img元素,然后获得img元素的data-src(也可以约定为其他属性名)属性的值,并赋予img的src,从而实现动态加载图片的机制. 这里需要注意的是: img在初

异步加载:ControlJS让脚本加载更快的一个模块

文章简介:关于ControlJs的使用和基础讲解. 关于ControlJs一共有三篇文章,这是第一部分.ControlJS是让脚本加载更快的一个模块(a javascript module for making scripts load faster). 三篇文章的结构分别为: 1. async loading2. delayed execution3.overriding document.write关于第一部分的异步加载,这个的关键在于尽快将页面作为html绘制出来,然后再用javascri

实现图片预加载的三大方法

  预加载图片是提高用户体验的一个很好方法.图片预先加载到浏览器中,访问者便可顺利地在你的网站上冲浪,并享受到极快的加载速度.这对图片画廊及图片占据很大比例的网站来说十分有利,它保证了图片快速.无缝地发布,也可帮助用户在浏览你网站内容时获得更好的用户体验.本文将分享三个不同的预加载技术,来增强网站的性能与可用性.   方法一:用CSS和JavaScript实现预加载   实现预加载图片有很多方法,包括使用CSS.JavaScript及两者的各种组合.这些技术可根据不同设计场景设计出相应的解决方案

jquery显示loading图片直到网页加载完成的方法

  本文实例讲述了jquery显示loading图片直到网页加载完成的方法.分享给大家供大家参考.具体实现方法如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <!DOCTYPE html> <html class="no-js"> <head> <meta charset='UTF-8'> <title>Simpl