RequireJS的几个关键问题

RequireJS的几个关键问题

依赖模块的路径解析

一般来说,RequireJS的路径解析方式为baseUrl+paths。
但是以下的情况例外:
* 以 “.js” 结束
* 以 “/” 开始
* 包含 URL 协议, 如 “http:” or “https:”

除了这些例外的情况之外,所有的路径都需要解析,解析的过程如下:

Created with Raphaël 2.1.0 Start 获取moduleId,既模块的识别字符串 检查paths配置,根据配置解析成正确的路径(正确的路径不是识别字符串,识别字符串是最初的字符串,也就是moduleId) b 获取模块文件 End

这个过程与后面命名模块的内容有相关性,可以解释为什么最好不要定义命名模块
如果paths没有配置,或者配置和路径一致(效果和没配置一样),那么moduleID和正确的路径就是一致的
以上的过程是moduleId的识别过程,但是这产生了一个疑问:是不是所有的模块都会进行这样一个解析过程?
现在我们来探讨这个问题:
目录结构:
* index.html
* js
* first
* a.js
* b.js
* another
* a.js
* b.js
* main.js

首先,最简单的依赖:

    // js/first/a.js
    define([],function(b){
    return {
        show:function(text){
            console.log(b+text+" world!");
        }
    }
});
    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{

        a:"first/a"

    }
});

require(["a"], function(a){

    a.show("hello");

});

此时浏览器的请求地址为/js/first/a.js, 可以看到moduleId “a”被解析为baseUrl+paths:”js”+”/”+”first/a”+”.js”,所以这里的moduleId是会被解析的.

接下来,我们更改main.js的moduleId:

    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{

        a:"first/a"

    }
});

require(["./a"], function(a){

    a.show("hello");

});

解析地址为 /js/first/a.js。

    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{

        a:"first/a"

    }
});

require(["../a"], function(a){

    a.show("hello");

});

解析地址为 /a.js, 请求失败。

    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{

        a:"first/a"

    }
});

require(["../js/a"], function(a){

    a.show("hello");

});

请求地址为 /js/a.js, 请求失败。

由上可知,对于moduleId的解析,如果解析的最高层地址不是baseUrl,那么paths的解析就会放弃,如同例外情况一样。所以要应用paths解析,就必须是的moduleId的第一层地址为baseUrl,也就是不可以出现”../”开头。所以例外情况要加多一个:
* 以 “.js” 结束
* 以 “/” 开始
* 包含 URL 协议, 如 “http:” or “https:”
* 以 “../” 开始(其实是会执行paths解析的)
此外,在参与解析的moduleId中的别名不一定都会进行paths解析:

    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{

        a:"first/a",
        b:"test"

    }
});

require(["b/a"], function(a){

    a.show("hello");

});

此时的解析地址为/js/test/a.js,也就是b被解析成”test”,但是a没有被解析成”first/a”,也就是说一个moduleId的paths解析,只会应用一次,以moduleId的出现顺序为准,上面的例子改为:

    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{

        b:"test",
        a:"first/a"


    }
});

require(["a/b/a"], function(a){

    a.show("hello");

});

解析地址为 /js/first/a/b/a.js, 第一个a被解析为first/a, 而第一个b和第二个a都不再被paths解析,所以,moduleId的第一个paths解析匹配项会被paths解析,后面的即使存在匹配项,也不再被解析。
那么现在存在另一个问题,如果决定第一个匹配项?

    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{

        "a/b":"haha",
        b:"test",
        a:"first/a",
        //"a/b":"xx" change to /js/xx/a.js, last a/b will valid, but the sequence between a, b and a/b will not influence

    }
});

require(["a/b/a"], function(a){

    a.show("hello");

});

解析地址为 /js/haha/a.js, 所以第一个匹配项是a/b,而不是a,所以第一个匹配项的寻找规则:最长匹配。

    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{

        "a/b":"haha",
        b:"test",
        a:"first/a",
        "a/b":"xx",
        "a/b/a":"kk"


    }
});

require(["a/b/a"], function(a){

    a.show("hello");

});

最长匹配是a/b/a, 所以解析地址为 /js/kk.js.

在这里,我们纠正一个之前的错误的解释。前面提到以”../”开头的moduleId不进行paths解析,其实是错误的,”../”并不在官方的例外列表里,所以它是会进行paths解析的。

    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{

        a:"first/a"

    }
});

require(["../js/a"], function(a){  // 解析结果为/js/a.js

    a.show("hello");

});
    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{

        a:"first/a"

    }
});

require(["./a"], function(a){  // 解析结果为/js/first/a.js

    a.show("hello");

});

事实上,我们知道,”./a” 和 “../js/a” 其实是同一个地址,但是解析结果却不一样?
如果匹配是以从头开始的最长匹配,中间的不匹配,那么”a”是匹配的,”./a”和”../js/a” 是不匹配的,但是结果是”./a”是匹配的,结果和”a”的匹配结果一样,而”../js/a”里面的a是不匹配的。
那是因为,”./a” 其实等价于”a”,所以会匹配到a,而”../js/a”将需要匹配”../”, 如果我们增加一个这样的paths规则:

// main.js
    requirejs.config({

    baseUrl:"js",
    paths:{

        a:"first/a",
        "../js":"first"

    }
});

require(["../js/a"], function(a){  // 解析结果为/js/first/a.js

    a.show("hello");

});

此时的解析结果为 js/first/a.js, 所以这个说明了不是”../js/a”不进行paths匹配,而是../作为匹配的前缀了,而”./a”中的./是不作为匹配前缀的。

所以我们得到了全局require函数依赖moduleId的匹配的最终结果:

一下情况不进行paths解析:
* 以 “.js” 结束
* 以 “/” 开始
* 包含 URL 协议, 如 “http:” or “https:”

以”./”开头的moduleId等价于去掉”./”,进行paths匹配是不将”./”作为匹配起点。
其余情况,moduleId的值作为paths解析的匹配检查值,与paths规则进行最长匹配检查,存在等长度的匹配,去最后一个。

paths匹配从第一个字符开始作最长匹配,不支持中间匹配(这是只匹配一个的原因,因为第二个匹配必然已经不是第一个字符开始了)。

这就是全局require函数依赖的paths解析规则。

接下来,我们讨论另一种情况,之前我们的讨论都是基于require函数的依赖moduleId,但是存在另一种不同于此的情况,那就是以依赖的依赖存在的情况,或者说,是次级依赖,以模块内部define函数的依赖列表的形式被依赖。

    // js/first/a.js
    define(["b"],function(b){

    return {

        show:function(text){

            console.log(b.name+text+" world!");
        }
    }

});
    // js/first/b.js
    define({name:"Roger: "});
    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{
        a:"first/a",
        b:"first/b"

    }
});

require(["a"], function(a){

    a.show("hello");

});

此时的解析地址为 /js/first/a.js 和 /js/first/b.js,可以看到,a的依赖b的moduleId也被paths解析了得到正确的地址。
输出结果:Roger: hello world!

如果更改a中b的moduleId:

define(["./b"],function(b){

    return {

        show:function(text){

            console.log(b.name+text+" world!");
        }
    }

});

解析结果不变。

    define(["../b"],function(b){

    return {

        show:function(text){

            console.log(b.name+text+" world!");
        }
    }

});

b的解析结果为/b.js,这个和出现在require函数依赖列表的情况是一样的,也就是以../开头的moduleId将不会进行解析,所以我们看到,即使以../b参与paths解析,假定结果是/first/b.js,虽然这也是错误的地址,但是根本连这个地址都不会出现,而是直接保留了/b.js,所以,出现在define依赖数组中的moduleId是否进行paths解析的条件和require函数依赖数组的moduleId一样:
* 以 “.js” 结束
* 以 “/” 开始
* 包含 URL 协议, 如 “http:” or “https:”
* 以 “../” 开始

这四种情况不进行paths解析。
但是,对于define函数的依赖数组的moduleId解析,远没有require的那么简单。
出现在全局require函数的依赖(局部的require函数与全局require的区别在后面讨论),其必然是相对于baseUrl的(除了不被解析的部分),但是出现在define函数的依赖,其当前路径却不一定是相对于baseUrl的。
事实上,其当前路径是父模块的当前路径,其当前路径等于baseUrl的充要条件是:其父模块的当前路径等于baseUrl。
注意,这里的当前路径指的是moduleId的当前路径,也就是一个模块的moduleId的最底层书写路径,是未解析前的路径。

    // main.js
    requirejs.config({

    baseUrl:"js",
    paths:{
        a:"first/a",
        b:"first/b",
        "c/d":"first"

    }
});

require(["c/d/a"], function(a){

    a.show("hello");

});

此时的a的解析路径为 /js/first/a.js,正确,a的moduleId为”c/d/a”,a的当前路径为”c/d”。
此时下面的两个写法事实上都是同一个模块:


    require(["c/d/a"], function(a){

    a.show("hello");

});

    //
require(["a"], function(a){

    a.show("hello");

});

但是这两种写法的moduleId和当前路径确实不同的。
“c/d/a”: moduleId为”c/d/a”, 当前路径为”c/d”;
“a”: moduleId为”a”, 当前路径为”./”,或者可以理解为”“。
这两种写法对于a的加载都是适用的,但是对于a的依赖却会产生影响。
究其原因,是因为在全局require函数中,”./”和”“是等价的,但是在define函数中,两者却不等价。

    // main.js
requirejs.config({

    baseUrl:"js",
    paths:{
        a:"first/a",
        //b:"first/b",
        "c/d":"first",
        "../js":"first"

    }
});

require(["../js/a"], function(a){

    a.show("hello");

});
    // js/first/a.js

define(["b"],function(b){

    return {

        show:function(text){

            console.log(b.name+text+" world!");
        }
    }

});

此时”b”的匹配结果为 js/b.js, 等价于require在的”b”,因为在paths中找不到匹配,所以直接就是baseUrl+”b”+”.js”。

如果修改为:

        // js/first/a.js

define(["./b"],function(b){

    return {

        show:function(text){

            console.log(b.name+text+" world!");
        }
    }

});

这解析结果为: js/first/b.js, 因为此时的b的moduleId为”../js/b”, 所以匹配了paths中的”../js”, 所以结果是baseUrl + “first” + “b”。
也就是”./b” 和 “b” 在define中是不等价的,./会加上父模块的当前路径,而这个当前路径是paths解析前的路径。
同样的,”../” 在define中也会加上父模块的当前路径去参与paths的解析。
而”b”这不会加上父模块的当前路径,直接以baseUrl作为当前路径,而以”b”去查找paths匹配。
require和define中对这个的处理实际上是有没有父模块的区别,require中的依赖没有父模块,而define的依赖存在父模块。
可以参考:
http://www.cnblogs.com/yexiaochai/p/3768570.html
http://www.cnblogs.com/chyingp/p/requirejs-path-resolve.html

这里,先提出一个问题,在define中的依赖,其moduleId是什么?
在require中,moduleId就是字面值,就是参与paths匹配的值,但是define中,参与paths匹配的却不一定是字面值,其实就是父模块的当前路径可能会加上去,所以这个时候的moduleId是哪个?如果模块存在命名,这个时候的命名需要和哪一个相等?这个会在后面的命名模块中讨论。

循环依赖的理解

在RequireJS中,循环依赖是一种需要特别处理的依赖。
一般来说,循环依赖是不提倡的,但是我们可能要处理已经存在的循环依赖。
必须知道的一点就是:并不是所有的循环依赖都可以处理,或者说,只有特定的循环依赖可以存在而不会出错。
这些循环依赖的共同特点就是:依赖循环,但是调用不循环,或者循环调用有终止条件,而且调用只存在于初始化之后(其实不一定)。
在很多语言中,循环依赖是无法解决的。JavaScript自身的特性使得它可以允许部分的循环依赖存在。

    // a.js
    define(["./b"],function(b){

  console.log("b in a define:"+ b.name);  // b.name == "Roger"
   b.show(); // error
    return {

        show:function(text){

            console.log(b.name+text+" world!");
        }
    }

});
    // b.js
define(["./a"],function(a){

    console.log("a in b define "+a);  // undefined

    return {

        show:function(){

            return a.show("hello");
        },
        name:"Roger"
    }

});

这个循环依赖可以正常初始化(无论是从a开始还是从b开始),假设从a开始,尽管在b的初始化过程中,其所声明的依赖a是undefined(因为a在b之后才完成了初始化),但是a的引用是正确的,所以b的初始化可以使用a的引用,但是不能使用a的属性和方法,因为是undefined。
这个循环依赖的初始化过程:

Created with Raphaël 2.1.0 Start a.js 初始化开始,划定内存区,确定a的引用地址 寻找a模块的依赖,找到b.js b.js 初始化开始,划定内存区,确定b的引用地址 b.js使用a的引用地址完成初始化(不能马上使用a的属性) a.js得到b模块,是否可以马上使用b的属性具体看这个循环依赖的特点(后论述),完成a模块初始化,此时a的引用地址写入变量,地址不变,但是不再是undefined a.js完成初始化,返回a模块 End

可以看到,尽管a模块在完成初始化之前的值都是undefined,但是其引用地址是在一开始就确定了的,所以b模块才能在a模块完成初始化之前就使用a的引用。
但是b模块无法使用a的任何属性,但是a是可以有条件地使用b模块的属性的(b.name),那是因为b.name并没有使用a的属性,而b.show这个方法使用了a的属性,而此时a尚未初始化,所以b.show是无法访问a的属性的,所以报错。

所以我们能够知道,循环依赖初始化顺利进行的前提是,次级依赖可以获取上层依赖的引用地址(尽管不能使用里面的属性)。
那么,次级依赖获取上层引用的方式有以下几种:

通过require模块获取
    define(["require", "a"],
    function(require, a) {
        //"a" in this case will be null if a also asked for b,
        //a circular dependency.
        return function(title) {
            return require("a").doSomething();
        }
    }
);

通过局部的require方法可以获取已经加载过的任何模块(使用moduleId,依赖的依赖的moduleId见后叙), 这个依赖在不在这个循环依赖的环里被加载都可以,但是一定要已经加载。
这种方式,其实不再需要书写依赖数组:

    // a.js
    define(["./c","./b"],function(b){
    console.log("b in a define:"+ b.name);
    //b.show();
    return {

        show:function(text){

            console.log(b.name+text+" world!");
        }
    }

});
    // b.js
    define(["require"],function(require){


    //console.log("a in b define "+a);
    console.log(require("./c").name);

    return {

        show:function(){

            return require("a").show("hello");
        },
        name:"Roger"
    }

});

上面的例子直接使用了require获取了先于b模块初始化之前就已经初始化完成的c模块,已经在a初始化后再获取的a模块。
注意a,c模块都没有以依赖列表的形式出现在b的define函数中。这种形式更多地习惯写成CommonJS的形式,这也说明了CommonJS的形式和依赖数组的形式的一致性。
注意,局部require函数不会引入新的模块,只会读取已经缓存的模块

CommonJS形式:
    //Inside b.js:
define(function(require, exports, module) {
    //If "a" has used exports, then we have a real
    //object reference here. However, we cannot use
    //any of a's properties until after b returns a value.
    var a = require("a");

    exports.foo = function () {
        return a.bar();
    };
});

或者以依赖数组的形式来使用exports:

    //Inside b.js:
define(['a', 'exports'], function(a, exports) {
    //If "a" has used exports, then we have a real
    //object reference here. However, we cannot use
    //any of a's properties until after b returns a value.

    exports.foo = function () {
        return a.bar();
    };
});
依赖数组传入

获取上层引用地址的方法,终究来讲,只有两种形式:
* 使用依赖数组形式,在初始化函数的参数中获取
* 使用依赖数组引入require,通过require去获取已经缓存的依赖的引用
* 使用CommonJS风格,通过require去获取已经缓存的依赖的引用

为什么是两种?因为第二中方法更像是第一种和第三种的结合,既声明了依赖数组,却又不把需要的依赖写入数组(或者写了不用),而是通过require去获取,所以倒不如直接使用CommonJS风格提供的require去获取,从而去掉依赖数组,或者是使用第一种,直接把依赖写入依赖数组。

这两种方法任何时候都可以使用吗?不是,使用require去获取模块引用时,总是实时地去查找那个模块的缓存引用地址,而这个缓存的地址是在模块初始化完成后才会写入的,所以在一个模块尚未完成初始化时,是获取不到的,究其原因,是因为模块的真正的引用地址是有可能改变的,必须在完成初始化后才能真正确定。但是使用依赖数组,从参数里获取依赖,这可以在上层模块初始化完成前(次级依赖初始化时,上层依赖必然没有完成初始化)就获得引用,但是这个引用是不可靠的(但是我们可以约定我们的代码是的其可靠,那就是使用exports)。
我们要从模块的初始化流程说起:

Created with Raphaël 2.1.0 Start 分配地址 初始化 使用return返回初始化结果? 使用return返回的结果作为模块 End 使用最初分配的地址,也就是export指向的地址,也就是modle.export作为模块 yes no

在分配地址完成之后,如果存在依赖,那么就会转去初始化依赖,而这个分配了的地址,就是依赖对上层模块的引用,也就是以依赖数组形式引入的上层依赖在函数参数的那个引用,而我们知道,这个值是作为上层依赖的引用地址存在的,但是可能在上层依赖完成初始化时(此时次级依赖早已完成初始化)被改写,所以如果次级依赖使用了这个地址,那么就可能出错,因为真正的引用地址可能会改变,当然我们可以约定使用export而不是return,那么这个值就不会改变。
require得到的总是最后那个正确的值,所以require的运行一定要在要找的模块完成初始化之后。

所以,用什么方式获取上层引用?
* 如果你确定使用的是exports,那么依赖数组的参数是有效的。
* 如果不确定是否会采用return,那么久使用require,CommonJS风格或者以require为依赖数组都可以,当然,使用exports也是可以用require方式的。

接下来,我们讨论什么循环依赖可以正确处理,而什么不可以。
我们之前提到了可以正确处理的循环依赖的条件:依赖循环,但是调用不循环,或者循环调用有终止条件,而且调用只存在于初始化之后。
* 依赖循环,这个好理解,就是存在循环的依赖
* 调用不循环,或者循环调用有终止条件:这个条件的意思就是,尽管模块存在依赖,但是其内部的方法调用其实不存在环,或者是存在环的时候,这个环有终止条件
* 调用只存在于初始化之后:意思就是,虽然a->b->a,但是b在初始化的时候,其实并没有马上调用a的属性,而是在a也完成初始化后才会发生调用的事件(但是a可以有条件地调用b的属性,条件就是那个属性不是一个a,b之间的环属性,或者,这个环属性所需要的环在a初始化完成前已经完成(这种情况b必然是使用依赖数组来获取a的引用的)),事实上是js可以把function返回的机制导致了这种情况的发生,否则,这种情况不会发生,那么可以正确处理的循环依赖也就不存在

我们举例说明每一种情况:
调用不依赖

命名模块的ModuleID

Shim模块

按需加载与软依赖

JSONP

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值