seajs 源码解读

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

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

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 源码解读

时间: 2024-10-13 03:15:47

seajs 源码解读的相关文章

RequireJs 源码解读及思考

写在前面: 最近做的一个项目,用的require和backbone,对两者的使用已经很熟悉了,但是一直都有好奇他们怎么实现的,一直寻思着读读源码.现在项目结束,终于有机会好好研究一下. 本文重要解读requirejs的源码,backbone源码分析将会随后给出. 行文思路: requirejs 基本介绍 requirejs使用后的几个好奇 requirejs源码解读 requirejs基本介绍 由于篇幅有限,这里不会详解requirejs的使用和api,建议读者朋友自己去用几次,再详读api.

QCustomplot使用分享(二) 源码解读

一.头文件概述 从这篇文章开始,我们将正式的进入到QCustomPlot的实践学习中来,首先我们先来学习下QCustomPlot的类图,如果下载了QCustomPlot源码的同学可以自己去QCustomPlot的目录下documentation/qcustomplot下寻找一个名字叫做index.html的文件,将其在浏览器中打开,也是可以找到这个库的类图.如图1所示,是组成一个QCustomPlot类图的可能组成形式. 一个图表(QCustomPlot):包含一个或者多个图层.一个或多个ite

vue源码解读预热-0

vueJS的源码解读 vue源码总共包含约一万行代码量(包括注释)特别感谢作者Evan You开放的源代码,访问地址为Github 代码整体介绍与函数介绍预览 代码模块分析 代码整体思路 总体的分析 从图片中可以看出的为采用IIFE(Immediately-Invoked Function Expression)立即执行的函数表达式的形式进行的代码的编写 常见的几种插件方式: (function(,){}(,))或(function(,){})(,)或!function(){}()等等,其中必有

SpringMVC源码解读 - RequestMapping注解实现解读 - RequestCondition体系

一般我们开发时,使用最多的还是@RequestMapping注解方式. @RequestMapping(value = "/", param = "role=guest", consumes = "!application/json") public void myHtmlService() { // ... } 台前的是RequestMapping ,正经干活的却是RequestCondition,根据配置的不同条件匹配request. @Re

jdk1.8.0_45源码解读——HashMap的实现

jdk1.8.0_45源码解读——HashMap的实现 一.HashMap概述 HashMap是基于哈希表的Map接口实现的,此实现提供所有可选的映射操作.存储的是<key,value>对的映射,允许多个null值和一个null键.但此类不保证映射的顺序,特别是它不保证该顺序恒久不变.  除了HashMap是非同步以及允许使用null外,HashMap 类与 Hashtable大致相同. 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能.迭代col

15、Spark Streaming源码解读之No Receivers彻底思考

在前几期文章里讲了带Receiver的Spark Streaming 应用的相关源码解读,但是现在开发Spark Streaming的应用越来越多的采用No Receivers(Direct Approach)的方式,No Receiver的方式的优势: 1. 更强的控制自由度 2. 语义一致性 其实No Receivers的方式更符合我们读取数据,操作数据的思路的.因为Spark 本身是一个计算框架,他底层会有数据来源,如果没有Receivers,我们直接操作数据来源,这其实是一种更自然的方式

jdk1.8.0_45源码解读——Set接口和AbstractSet抽象类的实现

jdk1.8.0_45源码解读——Set接口和AbstractSet抽象类的实现 一. Set架构 如上图: (01) Set 是继承于Collection的接口.它是一个不允许有重复元素的集合.(02) AbstractSet 是一个抽象类,它继承于AbstractCollection.AbstractCollection实现了Set中的绝大部分函数,为Set的实现类提供了便利.(03) HastSet 和 TreeSet 是Set的两个实现类.        HashSet依赖于HashMa

线程本地变量ThreadLocal源码解读

  一.ThreadLocal基础知识   原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板类并未采用线程同步机制,因为线程同步会影响并发性和系统性能,而且实现难度也不小. ThreadLocal在Spring中发挥着重要的作用.在管理request作用域的bean,事务管理,任务调度,AOP等模块中都出现了它的身影. ThreadLocal介绍: 它不是一个线程,而是线程的一个本地化

hadoop源码解读namenode高可靠:HA;web方式查看namenode下信息;dfs/data决定datanode存储位置

点击browserFilesystem,和命令查看结果一样 当我们查看hadoop源码时,我们看到hdfs下的hdfs-default.xml文件信息 我们查找${hadoop.tmp.dir}这是引用变量,肯定在其他文件有定义,在core-default.xml中查看到,这两个配置文件有个共同点: 就是不要修改此文件,但可以复制信息到core-site.xml和hdfs-site.xml中修改 usr/local/hadoop 是我存放hadoop文件夹的地方 几个关于namenode的重要文