新航路师徒学院

 找回密码
 立即注册
搜索
查看: 525|回复: 0

【新航路】seajs 源码解读

[复制链接]

80

主题

114

帖子

560

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
560
发表于 2016-8-31 12:43:57 | 显示全部楼层 |阅读模式
  1. 之前面试时老问一个问题seajs 是怎么加载js 文件的

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

  3. seajs 源码解读

  4. seajs 简单介绍



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

  7. 目录结构

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

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

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

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

  22. (function(global, undefined) {
  23.     if (global.seajs) {
  24.       return
  25.     }
  26.     // ....
  27. })(this);
  28. 其他文件的用途就不一一重复叙述了,看列表即可。

  29. 页面如何动态加载js文件



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

  31. var script = document.createElement('script');
  32. script.setAttribute('src', 'example.js');
  33. script.onload = function() {
  34.     console.log("script loaded!");
  35. };
  36. document.body.appendChild(script);
  37. 上述代码即可以完成一次简单的动态脚本加载。然而,seajs真正的核心在于处理模块依赖的问题。在前端JS开发领域,尤其是复杂的web应用,模块依赖问题一直是令人头疼的问题。
  38. 很简单的道理,例如A、B、C、D四个模块对应于A.js、B.js、C.js、D.js四个文件。他们之间的依赖关系例如以下:

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

  42. 模块化实现思路

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

  44. Module.define = function (id, deps, factory) {
  45.     // 获取代码中声明的依赖关系
  46.     deps = parseDependencies(factory.toString());
  47.     // 保存
  48.     Module.save();
  49.     // 匹配到url
  50.     var url = Module.resolve(id);
  51.     // 加载脚本
  52.     script.url = url;
  53.     loadScript();
  54.     // 执行factory并保存模块的引用
  55.     ...
  56. };
  57. 获取代码中声明的依赖

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

  59. define('scripts/a', function(require, exports, module) {
  60.     var factory = function() {
  61.         var moduleB = require('scripts/b');
  62.         ...
  63.     };
  64.     module.exports = factory;
  65. });
  66. 那么需要获取依赖的信息,我们可以借助Function的toString方法,一个函数的toString方法是会返回函数本身的代码的(对于JavaScript自身的函数,会返回[native code])。只需要正则表达式来匹配require关键词后面的引用关系即可。所以seajs中函数parseDependencies的写法就像这样(这一部分代码在util-deps.js):

  67. var SLASH_RE = /\\\\/g
  68. var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g
  69. function parseDependencies(code) {
  70.   var ret = []
  71.   code.replace(SLASH_RE, "")
  72.         // 匹配require关键词,找出依赖关系
  73.       .replace(REQUIRE_RE, function(m, m1, m2) {
  74.         if (m2) {
  75.           ret.push(m2)
  76.         }
  77.       })
  78.   return ret
  79. }
  80. 通过id来匹配脚本的url地址

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

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

  84.   id = parseAlias(id)
  85.   id = parsePaths(id)
  86.   id = parseVars(id)
  87.   id = normalize(id)

  88.   var uri = addBase(id, refUri)
  89.   uri = parseMap(uri)

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

  93. 模块加载过程

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

  95. var STATUS = Module.STATUS = {
  96.   // 1 - The `module.uri` is being fetched
  97.   FETCHING: 1,
  98.   // 2 - The meta data has been saved to cachedMods
  99.   SAVED: 2,
  100.   // 3 - The `module.dependencies` are being loaded
  101.   LOADING: 3,
  102.   // 4 - The module are ready to execute
  103.   LOADED: 4,
  104.   // 5 - The module is being executed
  105.   EXECUTING: 5,
  106.   // 6 - The `module.exports` is available
  107.   EXECUTED: 6
  108. }
  109. 也就是:
  110. * FETCHING 开始加载当前模块
  111. * SAVED 当前模块加载完成并保存模块数据
  112. * LOADING 开始加载依赖的模块
  113. * LOADED 依赖模块已经加载完成
  114. * EXECUTING 当前模块执行中
  115. * EXECUTED 当前模块执行完成

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

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

  118. Module.use 构造一个没有factory的模块,开始整个加载流程,状态初始化为FETCHING到SAVED;
  119. Module.prototype.load 通过load方法,开始加载子模块,状态由SAVED到LOADING;
  120. Module.prototype.onload 当子模块都加载完成后都会调用onload方法,状态由LOADING到LOADED;
  121. Module.prototype.exec 加载过程都结束了,开始执行模块,状态由EXECUTING到EXECUTED;
  122. 这里每个方法的详细过程就不一一解析,有兴趣的同学可以去看源码。
  123. 实际上,seajs会对加载过的模块保存一份引用在cachedMods中,在require的时候会先调用缓存中的模块。

  124. seajs.require = function(id) {
  125.   var mod = Module.get(Module.resolve(id))
  126.   if (mod.status < STATUS.EXECUTING) {
  127.     mod.onload()
  128.     mod.exec()
  129.   }
  130.   return mod.exports
  131. }
  132. Module.get = function(uri, deps) {
  133.   return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
  134. }
  135. 总结

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

  137. 转自:http://blog.segmentfault.com/civerzhu/1190000000471722
复制代码


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

新航路师徒学院 ( 京ICP备16035622号

GMT+8, 2018-10-21 18:04 , Processed in 0.127661 second(s), 22 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表