您的位置:首页 > Web前端 > JavaScript

seajs 3.0.0 源码阅读笔记

2016-07-10 10:28 471 查看
seajs 的源代码可以在 github上获取。seajs 在文档“如何参与开发”中说明了阅读顺序,当然为了便于阅读,在了解了目录结构之后,我直接阅读了合并好的 sea-debug.js。

整个seajs采用的是2空格缩进,避免分号的写法,我不是很习惯,但不影响阅读。


文件/目录结构

文档中各个源文件所包含的内容大致如下:

intro.js
 

文件头

sea.js
 

定义全局 
global.seajs
 对象和 
seajs.data
 对象

util-lang.js
,语言相关工具 

定义用于判断对象类型的 
isXxxxx()
 函数,以及一个与语言无关的 
cid()


util-events.js
 

定义 seajs 的事件处理相关函数,包括 
on()
off()
emit()


util-path.js
 

定义用于路径处理的工具函数

util-request.js
 

定义请求文件的工具函数 
seajs.request()
 等

util-deps.js
 

定义用于分析依赖关系的 
parseDependencies()


module.js
 

seajs 的核心,模块类。 

也包含部分 seajs 的方法

config.js
 

定义 
seajs.config()
,以及 
data
 部分属性的默认值

所以合并之后的整个 seajs 代码看起来就像这样

(function(global, undefiend) {

  global.seajs = {

    data: {}

  }

  var isObject = function() {}

  var isString = function() {}

  var isArray = function() {}

  var isFunction = function() {}

  var cid = function() {}

  data.events = {}

  seajs.on = function() {}

  seajs.off = function() {}

  seajs.emit = function() {}

  // path utils, and

  seajs.resolve = function() {}

  var loaderDir

  var parseDependencies = function() {}

  function Module() {}

  seajs.config = function() {}

})(this);


isXxxx 用于判断对象类型

首先是定义了一个产生 isXxxx 的函数工厂 
isType()


function isType(type) {

  return function(obj) {

    return {}.toString.call(obj) == “[object " + type + "]”

  }

}

从这个工厂的代码可以看出来,
isXxxx()
 主要是通过 
Object.prototype.toString
 的值来判断对象类型的。

当然也有例外:

// 毕竟 Array.isArray() 是 [native code],效果会
4000
高得多

var isArray = Array.isArray || isType(“Array”)

这里有几件事情我不是很明白:

就是为什么不使用 
typeof
 运算符来判断类型,一般语言中运算符实现会比比较字符串快得多。

{}.toString.call(obj)
 和 
Object.prototype.toString.call(obj)
 的作用是一样的,但是在运行时,每执行一次 
isXxxx
 就会产生一个新的 
{}
 对象;而 
Object.prototype
 始终都是同一个对象,似乎可以减少不少开销

jQuery 的 
isFunction()
 等方法都是通过 
jQuery.type()
 来实现的,而 
jQuery.type
 中则是通过定义了一个 
class2type
 字典对象来做类型映射,

jQuery.each(“Boolean Number String Function Array Date RegExp Object Error”.split(” “), function(i, name) {

    class2type[ “[object " + name + "]” ] = name.toLowerCase();

});

这种作法也会比每次都拼字符串(
"[object " + type + "]"
)来比较要效率高一些吧。

不过 seajs 和 jquery 都没有直接依赖 
typeof
 运算符来实现 
isXxxx
,我相信绝对不是偶然,一定有啥原因,不过这个原因我目前就不清楚了,希望玉伯能看到这个博客,稍作解释


事件

seajs 只为全局对象 
seajs
 添加了事件处理。事件的回调函数链保存在 
seajs.data.events
 中,以事件名称为
key,
Array
 对象保存的回调函数链为 value。

因为 seajs 的事件主要为了插件而定义,所以对参数并没有严格的校验。比如

// 自定义一个比较奇葩的事件,这不会报错

seajs.on(null, “fake callback”);

// 但是执行就会出错了

seajs.emit(“null”)

// 输出:TypeError: string is not a function

看样子,插件开发者得自己注意下这个问题了。

看完 seajs 的源码,大概定义了这么一些事件

error
,貌似跟 NodeJS 有点关系,没仔细看

load
,在模块对象状态变成 
LOADING
 后触发,参数是所有依赖模块的
URI。

  // Emit `load` event for plugins such as combo plugin

  var uris = mod.resolve()

  emit(“load”, uris)

exec
,在模块对象状态变成 
EXECUTED
 后触发,参数就是模块对象本身

fetch
,在模块对象状态刚变成 
FETECHING
 时触发,参数是一个临时对象
emitData
,事件结果保存在 
emitData.requestUri
,用于后面的 
request
 请求数据。

  // Emit `fetch` event for plugins such as combo plugin

  var emitData = { uri: uri }

  emit(“fetch”, emitData)

  var requestUri = emitData.requestUri || uri

request
,在 
fetch
 事件后对 
emitData.requestUri
|| uri
 进行了处理之后,通过 
seajs.request()
 请求数据之前触发,参数是一个临时对象,变量名复用的 
emitData
。事件处理完成后根据 
emitData.requested
 的值来判断是否需要调用 
seajs.request
 请求数据。

  // Emit `request` event for plugins such as text plugin

  emit(“request”, emitData = {

    uri: uri,

    requestUri: requestUri,

    onRequest: onRequest,

    charset: isFunction(data.charset)

      ? data.charset(requestUri) || ‘utf-8′

      : data.charset

  })

  if (!emitData.requested) {

    // …

  }

resolve
,在 
Module.resolve
 中调用 
seajs.resolve()
 之前触发,参数是一个临时对象 
emitData
。事件中如果产生了有效的 
emitData.uri
,则不再调用
seajs.resolve()


  // Emit `resolve` event for plugins such as text plugin

  var emitData = { id: id, refUri: refUri }

  emit(“resolve”, emitData)

  return emitData.uri || seajs.resolve(emitData.id, refUri)

config
,在 
seajs.config()
 中,完成对
config 对象的处理之后触发,参数就是 config 对象。

seajs.config = function(configData) {

  // …

  emit(“config”, configData)

  return seajs

}


Module 类

Module
 类才是 seajs 的重头戏,核心的核心。seajs 作为一个模块加载器,所以模块都是以一个 
Module
 对象保存在 
cachedMods
 中的。

var cachedMods = seajs.cache = {}

每个模块都有 8 种状态,它一定是在这 8 种状态之一,而且貌似状态改变还是不可逆的。

var STATUS = Module.STATUS = {

  // 没定义状态值为 0 的状态常量,这是初始状态

  // 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,

  // 7 – 404

  ERROR: 7

}

其中 
SAVED
 状态可以理解为 
FETCHED
 状态。除了初始状态 
0
 和错误状态 
ERROR:
7
 之外,其它 6 个状态都是成对出现的,即 
ING
 状态和 
ED
 状态,这三对状态清晰的划分出来三个处理过程:fetch、load、exec,对应于模块对象的
3 个方法:
fetch()
load()
exec()


从代码内容来看,这三个主要过程方法主要功能分别可以用一句话说明:

fetch
,从 URL 加载模块定义,得到 factory 函数,并将
factory 函数赋值给对应的模块对象(通过 
Module.get()
 创建或获取);

load
,fetch 并 load 所有依赖模块,并在保证所有依赖模块都是
LOADED 状态之后,调用入口模块(
_entry
)的 
callback
(貌似只有通过 
seajs.use()
 创建的匿名模块才有 
callback


exec
,在调用这个方法的时候,可以保证所有依赖模块都已经是 LOADED
状态了,所以 
exec
就只是简单的执行 factory 函数,并返回 
exports
。factory
只执行一次,然后将 
exports
 缓存下来。

现在来看看入口函数 
seajs.use()
、模块定义函数 
define()
、模块关系过程处理方法 
fetch()
,
load()
exec()
 的主要调用关系:

// seajs.use 只调用了 Module.use,所以它们的调用关系可以看作等同

seajs.use = Module.use = function() {

  Module.get()

  exec()        // 通过 _entry.callback 调用

  load()

}

define = Module.define = function() {

  Module.save(id, factory)

}

Module.prototype.fetch = function() {

  define()      // 通过 seajs.request() 调用

  load()

}

Module.prototype.load = function() {

  pass()

  fetch()

  // [递归]

  // 结束的条件是 _entry.remain === 1,

  // 即当前是最后一个依赖模块

  // 递归结束时调用 _entry.callback,即调用了 exec

  load()  

}

Module.prototype.exec = function() {

  exec()        // 通过 define 中定义的 factory 函数调用

}

Module.prototype.pass = function() {

  // [递归]

  // 结束条件是把 _entry 传递到最末一层依赖

  // 递归过程通过 _entry.remain 进行了引用记数

  pass()

}

各函数和方法具体处理过程看代码就明白了,因为递归关系有点复杂,还有一些回调关系在里面,所以看起来有点绕,不过还算是看得明白。
http://seajs.org 引用的 seajs 版本是 2.2.1,从这引页面的控制输出 
seajs.Modules.prototype
 来看,并没有定义 
pass()
 方法,所以对
_entry 的处理可能会有点不一样,稍后看了 seajs 2.2.3 版本的代码就知道了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  seajs 阅读