之前面试时老问一个问题seajs 是怎么加载js 文件的

在网上找一些资料,觉得这个写的不错就转载了,记录一下,也学习一下

seajs 源码解读

seajs 简单介绍


seajs是前端应用模块化开发的一种很好的解决方案。对于多人协作开发的、复杂庞大的前端项目尤其有用。简单的介绍不多说,大家可以到seajs的官网seajs.org参看介绍。本文主要简单地解读一下seajs的源码和模块化原理。如果有描述不实的地方,希望大家指正和交流。
注:本文的解析是基于seajs的2.2.1版本。

目录结构


解压seajs之后的src目录结构如下:

intro.js             -- 全局闭包头部
sea.js               -- 基本命名空间

util-lang.js         -- 语言增强
util-events.js       -- 简易事件机制
util-path.js         -- 路径处理
util-request.js      -- HTTP 请求
util-deps.js         -- 依赖提取

module.js            -- 核心代码
config.js            -- 配置
outro.js             -- 全局闭包尾部

src目录存放主要的seajs源代码。各个文件的作用也如上面所示。其中,module.js是这次源码解读的核心,但我也会顺带介绍一下其他文件的作用的。
sea.js对代码比较简单,其实就是声明一下全局的seajs命名空间。
intro.js和outro.js则是我们熟悉的匿名函数包裹基本代码的方式,只是这里比较特别的是,这段匿名函数被拆分成intro.js和outro.js两个文件。这样的做法主要是方便调试,在调试的环境下,不引用intro.js和outro.js即可以直接在全局里暴露seajs内部的接口,调试起来比较方便。intro.js和outro.js合并起来的代码如下:

(function(global, undefined) {
    if (global.seajs) {
      return
    }
    // ....
})(this);

其他文件的用途就不一一重复叙述了,看列表即可。

页面如何动态加载js文件


在解析seajs的源码和原理之前,让我们来回忆一下,在没有seajs或者requirejs的情况下,最原始的动态脚本加载方法是怎样的。方法很简单:其实就是创建一个script的标签,设置了src为你想要加载的脚本url,把script标签append到Dom里去就想了,so easy!没错,绝大部分模块加载js库的原理都是如此。

var script = document.createElement('script');
script.setAttribute('src', 'example.js');
script.onload = function() {
    console.log("script loaded!");
};
document.body.appendChild(script);

上述代码即可以完成一次简单的动态脚本加载。然而,seajs真正的核心在于处理模块依赖的问题。在前端JS开发领域,尤其是复杂的web应用,模块依赖问题一直是令人头疼的问题。
很简单的道理,例如A、B、C、D四个模块对应于A.js、B.js、C.js、D.js四个文件。他们之间的依赖关系例如以下:

  • A 依赖 B
  • B 依赖 C和D

问题在于,如何找出模块里的依赖关系,如何确保A在运行前已经加载了B等等。这些都是前端模块化和模块依赖需要解决的问题

模块化实现思路


seajs的模块化实现原理,说简单其实不简单,说复杂其实也不是很复杂。主要思路可以用下面这一段代码来说明:

Module.define = function (id, deps, factory) {
    // 获取代码中声明的依赖关系
    deps = parseDependencies(factory.toString());
    // 保存
    Module.save();
    // 匹配到url
    var url = Module.resolve(id);
    // 加载脚本
    script.url = url;
    loadScript();
    // 执行factory并保存模块的引用
    ...
};

获取代码中声明的依赖

首先我们来看看如何获取代码中声明需要依赖的模块。一般情况下,seajs中同步加载模块的写法是类似这样的:

define('scripts/a', function(require, exports, module) {
    var factory = function() {
        var moduleB = require('scripts/b');
        ...
    };
    module.exports = factory;
});

那么需要获取依赖的信息,我们可以借助Function的toString方法,一个函数的toString方法是会返回函数本身的代码的(对于JavaScript自身的函数,会返回[native code])。只需要正则表达式来匹配require关键词后面的引用关系即可。所以seajs中函数parseDependencies的写法就像这样(这一部分代码在util-deps.js):

var SLASH_RE = /\\\\/g
var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g
function parseDependencies(code) {
  var ret = []
  code.replace(SLASH_RE, "")
        // 匹配require关键词,找出依赖关系
      .replace(REQUIRE_RE, function(m, m1, m2) {
        if (m2) {
          ret.push(m2)
        }
      })
  return ret
}

通过id来匹配脚本的url地址

然后找出代码中声明的依赖id,通过id来匹配正确的脚本url地址。这一部分的代码在util-path.js

function id2Uri(id, refUri) {
  if (!id) return ""

  id = parseAlias(id)
  id = parsePaths(id)
  id = parseVars(id)
  id = normalize(id)

  var uri = addBase(id, refUri)
  uri = parseMap(uri)

  return uri
}

这里有个特别的地方,类似require('a/b/c')这样的写法,seajs是如何知道脚本地址的绝对路径的呢?道理很简单,就是通过seajs自己往dom里添加的id为'seajsnode'的script节点或者是当前html中最后一个script节点,通过这些节点的src属性获取脚本的绝对路径。

模块加载过程

让我们把目光移回到核心的module.js中。seajs为模块的加载过程定义了6种状态。

var STATUS = Module.STATUS = {
  // 1 - The `module.uri` is being fetched
  FETCHING: 1,
  // 2 - The meta data has been saved to cachedMods
  SAVED: 2,
  // 3 - The `module.dependencies` are being loaded
  LOADING: 3,
  // 4 - The module are ready to execute
  LOADED: 4,
  // 5 - The module is being executed
  EXECUTING: 5,
  // 6 - The `module.exports` is available
  EXECUTED: 6
}

也就是:
FETCHING 开始加载当前模块
SAVED 当前模块加载完成并保存模块数据
LOADING 开始加载依赖的模块
LOADED 依赖模块已经加载完成
EXECUTING 当前模块执行中
EXECUTED 当前模块执行完成

其实这一加载执行过程并非线性的,当前模块在加载所依赖的模块的是,所依赖的模块同样也需要进行这一过程,直到所有的依赖都加载执行完毕,当前模块才开始执行。

在module.js中seajs中的一些方法说明了上述整个流程。

  • Module.use 构造一个没有factory的模块,开始整个加载流程,状态初始化为FETCHING到SAVED;
  • Module.prototype.load 通过load方法,开始加载子模块,状态由SAVED到LOADING;
  • Module.prototype.onload 当子模块都加载完成后都会调用onload方法,状态由LOADING到LOADED;
  • Module.prototype.exec 加载过程都结束了,开始执行模块,状态由EXECUTING到EXECUTED;

这里每个方法的详细过程就不一一解析,有兴趣的同学可以去看源码。
实际上,seajs会对加载过的模块保存一份引用在cachedMods中,在require的时候会先调用缓存中的模块。

seajs.require = function(id) {
  var mod = Module.get(Module.resolve(id))
  if (mod.status < STATUS.EXECUTING) {
    mod.onload()
    mod.exec()
  }
  return mod.exports
}
Module.get = function(uri, deps) {
  return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
}

总结


前端模块化一直是前端开发中比较重要的一点。前端开发相对其他语言来说比较特殊,尤其是对应大型Web项目的前端代码,如何简洁优雅地划分模块,如何管理这些模块的依赖问题,这些都需要花一定的时间去认识和探讨。因此,Common.js(致力于设计、规划并标准化 JavaScript API)的诞生开启了“ JavaScript 模块化的时代”。前端领域的模块化方案,像requireJS、SeaJS等都是Common.js的实践者,对我们规划前端的代码很有帮助。然而,问题其实还有很多,seajs依然未能完全满足前端模块化开发,在性能问题、打包部署等方法还有着不足,不过技术的未来总在进步,相信以后会有更好的解决方法。

转自:http://blog.segmentfault.com/civerzhu/1190000000471722

seajs 源码解读的更多相关文章

  1. RequireJs 源码解读及思考

    写在前面: 最近做的一个项目,用的require和backbone,对两者的使用已经很熟悉了,但是一直都有好奇他们怎么实现的,一直寻思着读读源码.现在项目结束,终于有机会好好研究一下. 本文重要解读r ...

  2. seajs源码学习(一)

    今天是2015年12月4日,天气较为阴冷.(习惯性记下日期和天气,总要留些回忆给以后) 学习的最佳捷径是模仿,所以如果想快速提高javascript技术,捷径就是去读大神们的优秀源码.就像我们学说话一 ...

  3. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  4. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  5. SDWebImage源码解读 之 UIImage+GIF

    第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...

  6. SDWebImage源码解读 之 SDWebImageCompat

    第三篇 前言 本篇主要解读SDWebImage的配置文件.正如compat的定义,该配置文件主要是兼容Apple的其他设备.也许我们真实的开发平台只有一个,但考虑各个平台的兼容性,对于框架有着很重要的 ...

  7. SDWebImage源码解读_之SDWebImageDecoder

    第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...

  8. SDWebImage源码解读之SDWebImageCache(上)

    第五篇 前言 本篇主要讲解图片缓存类的知识,虽然只涉及了图片方面的缓存的设计,但思想同样适用于别的方面的设计.在架构上来说,缓存算是存储设计的一部分.我们把各种不同的存储内容按照功能进行切割后,图片缓 ...

  9. SDWebImage源码解读之SDWebImageCache(下)

    第六篇 前言 我们在SDWebImageCache(上)中了解了这个缓存类大概的功能是什么?那么接下来就要看看这些功能是如何实现的? 再次强调,不管是图片的缓存还是其他各种不同形式的缓存,在原理上都极 ...

随机推荐

  1. 安装glibc2.7

    参考链接http://fhqdddddd.blog.163.com/blog/static/18699154201401722649441/ /lib/libc.so.6: version `glib ...

  2. 缓存需要注意的问题以及使用.net正则替换字符串的方法

    参考资料:http://www.infoq.com/cn/news/2015/09/cache-problems 正则替换字符串的简单方法: var regTableType = new Regex( ...

  3. MVC个人认为的终极分页

    //传入要查询的字段,查询条件(例如根据姓名查看数据的数据筛选),按照什么排序,页码,信息条数 //T:要操作的类型 //Tkey:根据什么类型来排,ID的话返回的是int类型,但是name的话又会返 ...

  4. UVa11235 FrequentValues(RMQ)

    Problem F: Frequent values You are given a sequence of n integers a1 , a2 , ... , an in non-decreasi ...

  5. UVaLive 6697 Homework Evaluation (DP)

    题意:给出一个长字符串,再给一个短字符串,进行匹配,如果第i个恰好匹配,则 +8,:如果不匹配,可以给长或短字符串添加-,先后匹配,这样-3, 连续的长字符串添加-,需要减去一个4:也可不给添加-,则 ...

  6. hdoj 1404 Digital Deletions(博弈论)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1404 一看就是博弈论的题目,但并没有什么思路,看了题解,才明白 就是求六位数的SG函数,暴力一遍,打表 ...

  7. 28.怎样在Swift中实现单例?

    1.回忆一下OC中的单例实现 //AFNetworkReachabilityManager中的单例,省略了其他代码 @interface AFNetworkReachabilityManager : ...

  8. AP(应付帐管理)

    --更新供应商地点 PROCEDURE update_vendor_site(p_init_msg_list IN VARCHAR2 DEFAULT fnd_api.g_false, x_return ...

  9. Java字面常量与常量池

    Java中的字面常量(区别于final创建的有名常量)通常会保存在常量池中,常量池可以理解为像堆一样的内存区域.但是常量池有一个特性就是,如果常量池中已存在该常量将不会再次为该常量开辟内存 还是看个程 ...

  10. 采用Asp.Net的Forms身份验证时,非持久Cookie的过期时间会自动扩展

    问题描述 之前没有使用Forms身份验证时,如果在登陆过程中把HttpOnly的Cookie过期时间设为半个小时,总会收到很多用户的抱怨,说登陆一会就过期了. 所以总是会把Cookie过期时间设的长一 ...