之前面试时老问一个问题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. 常见mongo命令

    @(编程) 查询 db.getCollection('SalaryEntity').find({"Month" : "201601"}) db.getColle ...

  2. POJ3630Phone List(字典树)

    经典的字典树的题目了,这次完全是按照自己的风格来写的,没有参考其他人的代码风格来写. 分析:如果采用常规的暴力枚举,那么复杂度就是O(n*n*str.length) = O(10^9),这明显是会超时 ...

  3. [Oracle]配置path使oracle备份/导入数据命令exp/imp起作用

    将E:\Oracle11g\app\Administrator\product\11.2.0\dbhome_1\bin;路径添加如path

  4. iOS版本检测与版本升级

    14年苹果官方要求所有的APP不能出现 “当前版本”字样,是因为从iOS8系统开始,你可以在设置里面设置在WiFi情况下,自动更新安装的APP.此功能大大方便了用户,但是一些用户没有开 启此项功能,因 ...

  5. 一个可创建读取日志的管理类(可固定创建2M大小的日志文件)

    这里,将日志管理基类命名为LogManagerBase(抽象类),具体的不同类型的日志可以通过继承完成.该基类可将日志以每个2M的方式存储起来,并可以读取当前正在使用的日志的所有内容. 要实现该基类, ...

  6. CSS Layout

    fontline-heightcolormarginpaddingbordertext-alignbackground widthheightfloatcleardisplay 定位属性 属 性 描 ...

  7. Spket在Eclipse/MyEclipse下的安装和配置(图文教程)

    一.安装Spket 第一种方法:网上更新方式 1.插件首页:http://www.spket.com2.插件名称:Spket IDE3.更新连接(Update Site):http://www.spk ...

  8. linux下oracle11g R2的启动与关闭监听、数据库

    su - oracle           切换到oracle账户 lsnrctl start          启动监听 sqlplus /nolog     登陆sqlplus conn /as  ...

  9. IOS 7 Study - UIActivityViewController(Presenting Sharing Options)

    You want to be able to allow your users to share content inside your apps with theirfriends, through ...

  10. Codeforces Educational Codeforces Round 5 D. Longest k-Good Segment 尺取法

    D. Longest k-Good Segment 题目连接: http://www.codeforces.com/contest/616/problem/D Description The arra ...