avalonJS-源码阅读(前)

avalon模块加载

avalon自己实现了一套可被替换的模块加载系统(AMD loader)。具体什么是AMD loader可参看doJo官方博客关于AMD loader的翻译讲解,看完之后,再继续往下看,会比较清楚些。

模块加载配置

模块加载系统可替换原理参见javascript 闭包暴露句柄,可替换的前提是前面加载的amd loader文件将definerequire函数暴露给window对象。具体如何替换可执行

avalon.config.plugins.loader(false)//不推荐,但有效
//或者
avalon.config({loader:false})

当然了,如果更改了amd loader的话,可不要用avalon.config.plugins.text|css(url)了。
avalon.config.nocache可用来配置是否去除缓存,测试的时候
avalon可通过配置avalon.config.[shim|paths],来加载一些不符合amd加载规则的数据源,例如加载csdn上的jquery等。
paths意味着地址。这里需要注意的是,paths的设定时,需要:,也就是全路径,不要像这样www.abc.com,而是http:www.abc.com
shim则是一个object。

//shim
{//参考下面的数据结构
    $moduleId:{
        src:$url
        exports:$fn|$string // string说明加载的js文件会在window下存放引用,例如jquery, "$"==>window["$"]
        deps:...
    }
}

模块加载数据结构

模块加载所用到的数据先放出来,方便源码阅读推断。

//avalon.modules 存储模块信息
{
    $moduleId:{//默认值
        id:$string//随机生成或指定
        exports:$object|$string,//暴露出引用
        deps:$array|$object,//依赖的部件 为$object时,会添加当前部件的加载状态,依赖的deps 又都会在avalon.modules下有各自的状态记录
        state:$number//加载状态 2:加载过 1:正在加载
        factory:$fn//模块本身,会被存放到factorys中,他的this指代window对象
        args:$array//保存依赖模块的返回值
    }
}
//factorys数组下的factory函数
factory.delay=$fn//检查依赖,延迟加载
factory.id=$string//用来debug用的

require实现

require(deps, factory, baseUrl)
require 默认id的生成是通过"callback"+setTimeout("1")来实现UniId。
require 函数会先调用loadSource遍历deps是否都加载完,没加载完的,会根据加载文件类型调用不同的函数去完成异步加载,并将其要加载的依赖放入loadings存储,通过各种合适的时机(例如,完成一个js模块的加载时)调用checkDeps来将loadings处理掉,更新依赖状态,进而加载自己。
这里需要注意的是,factory的this,指代window对象(不知为何)。

动态加载

动态加载划分为三种:js、css、text(文本),分别对应一个函数。并对url #?后缀进行删除。
加载css时,前面添加 css! 。

css加载

css分两种加载,先介绍一个简单的,复杂的则是通过AMD loader加载的。加载方式是构造<link href='...' rel='stylesheet' id='$sepcialURL'>并插入到head最上边。这个地方有个疑问是,css样式覆盖会按照后来覆盖前面的样式来吗?有待测试。

text加载

text的加载是通过ajax做的,并将结果赋值给exports,保存在avalon.modules下面。

js加载

js加载会有对前面提到的avalon.config.shim进行处理(说实在的这个处理只有看完源码后方能使用无误。纠结~),具体的加载功能由loadJS函数来完成。
loadJS通过创建<script class='$日期'>节点并插入到head上来加载js文件,当加载完成后,会将class属性改写掉,并将define函数定义的factory函数和回调函数执行一下。如果加载失败了,写个日志呗。

define实现

define(id, deps, factory)
define的实现会牵扯到用户传参的循环依赖,例如:加载a需要先加载b,加载b又需要先加载a。所以上面的数据结构$moduleIddeps就变的十分有用了。根据这个参数的关系,就可找到是否存在循环依赖。
define还会调用require函数来实现JS文件的加载。
define还是require函数的属性值哦。innerRequire.define=function(){...}

ready!

ready!是avalon内置模块,主要用来等待游览器扫完dom树之后,再执行。avalon.ready也就是基于此的,上下avalon.ready源码。

avalon.ready = function(fn) {
    innerRequire("ready!", fn)//  require('ready!',fn);使用innerRequire是预防avalon的AMD loader被替代掉}

函数介绍

checkDeps

checkDeps是用来检查存放在loadings模块的依赖是否都完成加载,如果依赖都完成了加载,而自己没有,则去执行自己的factory工厂函数。这个函数用到了loop来跳过双for循环break问题,可以拿来借鉴下。

function checkDeps() {
    //检测此JS模块的依赖是否都已安装完毕,是则安装自身
    //只要loadings有一个
    loop: for (var i = loadings.length, id; id = loadings[--i];) {

        var obj = modules[id],
            deps = obj.deps
        for (var key in deps) {
            if (ohasOwn.call(deps, key) && modules[key].state !== 2) {
                continue loop
            }
        }
        //如果deps是空对象或者其依赖的模块的状态都是2
        if (obj.state !== 2) {
            loadings.splice(i, 1) //必须先移除再安装,防止在IE下DOM树建完后手动刷新页面,会多次执行它
            fireFactory(obj.id, obj.args, obj.factory)
            checkDeps() //如果成功,则再执行一次,以防有些模块就差本模块没有安装好
        }
    }
}

获取当前scriptURL

源码很棒且注释很全,直接上源码。作者司徒正美还在博文getBasePath 函数中进行了详细的讲解,有兴趣的可以去了解下。

//getCurrentScript(true);
function getCurrentScript(base) {
    // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
    var stack
    try {
        a.b.c() //强制报错,以便捕获e.stack
    } catch (e) { //safari的错误对象只有line,sourceId,sourceURL
        stack = e.stack
        if (!stack && window.opera) {
            //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
            stack = (String(e).match(/of linked script \S+/g) || []).join(" ")
        }
    }
    if (stack) {
        /**e.stack最后一行在所有支持的浏览器大致如下:
         *chrome23:
         * at http://113.93.50.63/data.js:4:1
         *firefox17:
         *@http://113.93.50.63/query.js:4
         *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
         *@http://113.93.50.63/data.js:4
         *IE10:
         *  at Global code (http://113.93.50.63/data.js:4:1)
         *  //firefox4+ 可以用document.currentScript
         */
        stack = stack.split(/[@ ]/g).pop() //取得最后一行,最后一个空格或@之后的部分
        stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, "") //去掉换行符
        return stack.replace(/(:\d+)?:\d+$/i, "") //去掉行号与或许存在的出错字符起始位置
    }
    var nodes = (base ? DOC : head).getElementsByTagName("script") //只在head标签中寻找
    for (var i = nodes.length, node; node = nodes[--i]; ) {
        if ((base || node.className === subscribers) && node.readyState === "interactive") {//subscribers="$"+(new Date-0)
            return node.className = node.src
        }
    }
}

一个有趣的加载测试

这个测试时关于加载顺序的。

<!DOCTYPE html>
<html>
<head>
<script src="avalon.js"></script>
</head>
<body>
<div ms-controller="hello">
<h1>Hello, {{name}}</h1>
</div>
<script>
avalon.ready(function(){
	var model = avalon.define('hello', function(vm){
        vm.name = 'Avalon';
    });
    avalon.scan();
})   
</script>
</body>
</html>

请问avalon什么时候将model刷进页面去的,为什么?和下面的代码有什么区别?

<!DOCTYPE html>
<html>
<head>
<script src="avalon.js"></script>
</head>
<body>
<div ms-controller="hello">
<h1>Hello, {{name}}</h1>
</div>
<script>
var model = avalon.define('hello', function(vm){
    vm.name = 'Avalon';
});
</script>
</body>
</html>

做完这个练习,我相信你一定会对dom加载有个新的认识。

小结

整个实现,为factory函数依赖注入废了不少劲,不知道添上angularJS的annotate函数,会不会简单些。


你可能感兴趣的:(avalonJS-源码阅读(前))