YUI Modules 与 AMD/CMD,哪一种方式更好?

YUI Modules 使用了”沙箱 + 命名空间“的方式;AMD 和 CMD 都使用了每个模块提供接口的方式,只不过是代码的执行时间不太相同。 对于 YUI Modules 和 AMD/CMD 的两种策略,孰优孰劣,不太好说。个人认为,从模块独立的角度来看,由于命名空间的存在,YUI Modules 的各个模块除了依赖关系外,还是有一定的耦合的。比如我们还是要小心各个模块之间的命名空间的约定,这就导致模块之间具有一定的复杂度。不知道我理解的对不对? 也…
关注者
262
被浏览
40,761

6 个回答

自己又学习整理了下,不一定正确,欢迎大家抛砖。

在 YUI2 的那个时代,我们学到了命名空间(namespace)的概念,同时也遗留了一些问题需要解决。

模块依赖关系
功能越来越复杂,模块文件越来越多,我们需要时刻小心模块之间的依赖。

多版本共存问题
由于遗留系统的缘故,我们有以下两个版本的 hello.js 需要共存:
<script src="path/to/my/hello/1.0/hello.js"></script>
<script src="path/to/my/hello/2.0/hello.js"></script>

这样,除非我们将它们各自的 API 放在不同的命名空间下,否则就会导致命名冲突(当然,你也可以在 2.0 版本的 hello.js 的 API 设计上做一些兼容性设计)。
YUI.hello.older.sayHello();
YUI.hello.newer.sayHello();

所以,YUI3来了,它解决了模块依赖的问题,并一定程度上解决了多版本共存问题。之间我有说过它使用了 Sandbox + namespace 的方式。

关于 Sandbox ,我们先举个栗子:
// path/to/my/hello/1.0/hello.js
YUI.add('hello', function(Y) {

    Y.sayHello = function(msg) {
        Y.DOM.set(el, 'innerHTML', 'Hello!');
    };
}, '1.0', {
    requires: ['dom']
});
// index.html
<div id="entry"></div>
<script>
YUI().use('hello', function(Y) {
    Y.sayHello('entry');   // <div id="entry">Hello!</div>
});
</script>

我们用到了两个接口,其中 YUI.add 看起来和 AMD 的 define 相似,而 YUI().use 和 require() 有个很大的不同,就是我们只有一个参数 Y ,那 Y 是什么呢?

Y是一个独立的构造函数实例,它存在于运行时,它包括了所有依赖的模块的 API。YUI().use 的执行分成两个步骤,首先是加载依赖模块,然后是将它们 attach 到 Y 上。看下面的栗子:
YUI.add('moduleA', function(Y) {

    Y.moduleA = function() {}
});

YUI.add('moduleA', function(Y) {

    Y.moduleB = {
        run: function() {}
        jump: function() {}
    };
});

YUI.add('moduleC', function(Y) {

    Y.play = function() {
        
    }
});


YUI().use(['moduleA', 'moduleB', 'moduleC'], function(Y) {
    console.log(Y.moduleA);
    Y.moduleB.run();
    Y.moduleB.jump();
    Y.play();
});
是不是有点像 $.extend(my, base) 呢?通过 Sandbox ,我们可以支持多版本共存,我们回到之前的 hello 例子,现在我们需要一个新的版本。
// path/to/my/hello/2.0/hello.js
YUI.add('hello', function(Y) {

    Y.sayHello = function(id) {
        var el = Y.DOM.byId(id);
        Y.DOM.set(el, 'innerHTML', 'Hello too!');
    };

}, '2.0', {
    requires: ['dom']
});
// index.html
<div id="entry"></div>
<script>
YUI().use('hello1.0', function(Y) {
    Y.sayHello('entry');   // <div id="entry">Hello!</div>
});
YUI().use('hello2.0', function(Y) {
    Y.sayHello('entry');   // <div id="entry">Hello too!</div>
});
</script>
两个版本的 hello.js 都存在于他们各自的 Sandbox 中,互不打扰。

但是呢, YUI3 并没有真正地实现多版本共存,请看下面的使用情况:
// index.html
<div id="entry"></div>
<script>
YUI().use(['hello1.0', 'hello2.0'], function(Y) {
    Y.sayHello('entry');   // <div id="entry">Hello too!</div>
});
我们又只能回到 YUI2 时代解决多版本共存的问题。

而且,YUI3 将所有的需要的模块 attach 到了同一个运行时构造实例,为了防止模块之间的命名冲突,所以我们仍然需要 namespace 这个函数,比如:
YUI.add('moduleA', function(Y) {
    // Y.run = function() {};
    Y.namespace('moduleA').run = function() {};
});

YUI.add('moduleA', function(Y) {
    // Y.run = function() {};
    Y.namespace('moduleB').run = function() {}
});

YUI.use(['moduleA', 'moduleB'], function(Y) {
    // Y.run = function() {};
    Y.moduleA.run();
    Y.moduleB.run();
});

而 AMD/CMD 就没有这个问题:
define('moduleA', function() {
    return {
        run: function() {}
    };
});

define('moduleB', function() {
    return {
        run: function() {}
    };
});

require(['moduleA', 'moduleB'], function(moduleA, moduleB) {
    moduleA.run();
    moduleB.run();
});


使用 AMD/CMD ,我们可以完全不使用命名空间这个概念,这样在大型的系统中,完全可以避免有深度的命名空间,以减少组织方式上的困难以及查找上的性能消耗。

通过自己的比较,感觉 AMD/CMD 比 YUI Module 要好很多,不知道 YUI Module 的优势是什么?
YUI那个没有研究过,不了解。不过我可以说下AMD/CMD的区别以及我的看法:
有必要简单提一下两者的主要区别,CMD推崇依赖就近,可以把依赖写进你的代码中的任意一行,例:
define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  var b = require('./b')
  b.doSomething()
})

代码在运行时,首先是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。具体做法是将function toString后,用正则匹配出require关键字后面的依赖。显然,这是一种牺牲性能来换取更多开发便利的方法。

而AMD是依赖前置的,换句话说,在解析和执行当前模块之前,模块作者必须指明当前模块所依赖的模块,表现在require函数的调用结构上为:
define(['./a','./b'],function(a,b){
   a.doSomething()
   b.doSomething()
}) 

代码在一旦运行到此处,能立即知晓依赖。而无需遍历整个函数体找到它的依赖,因此性能有所提升,缺点就是开发者必须显式得指明依赖——这会使得开发工作量变大,比如:当你写到函数体内部几百上千行的时候,忽然发现需要增加一个依赖,你不得不回到函数顶端来将这个依赖添加进数组。

细心的读者可能发现,到目前位置我讨论的AMD和CMD的思想的关于依赖的部分,都只讨论的“硬依赖”,也就是执行前肯定需要的依赖,但是这不是全部的情况。有的时候情况是这样的:
// 函数体内:
if(status){
  a.doSomething()
}

在这个函数体内,可能依赖a,也可能不依赖a,我把这种可能的依赖成为“软依赖”。对于软依赖当然可以直接当硬依赖处理,但是这样不经济,因为依赖是不一定的,有可能加载了此处的依赖而实际上没有用上。
对于软依赖的处理,我推荐依赖前置+回调函数的实现形式。上面的例子简单表述如下:
// 函数体内:
if(status){
  async(['a'],function(a){
    a.doSomething()
  })
}

至此可以对由commonJS衍生出来的方案做出总结了。在浏览器端来设计模块加载机制,需要考虑依赖的问题。
我们先把依赖分为两种,“强依赖” —— 肯定需要 和“弱依赖” —— 可能需要。
对于强依赖,如果要性能优先,则考虑参照依赖前置的思想设计你的模块加载器,我个人也更推崇这个方案一些;如果考虑开发成本优先,则考虑按照依赖就近的思想设计你的模块加载器。
对于弱依赖,只需要将弱依赖的部分改写到回调函数内即可。
如果现在我要实现一个模块加载器,我会将强依赖前置,弱依赖采用异步回调函数的形式,其它的方法我认为都只是语法糖而已,仅此就够了。

扩展阅读:blog.youyo.name/archive