seajs学习之模块的依赖加载及模块API的导出


Posted in Javascript onOctober 20, 2016

前言

SeaJS非常强大,SeaJS可以加载任意 JavaScript 模块和css模块样式,SeaJS会保证你在使用一个模块时,已经将所依赖的其他模块载入到脚本运行环境中。

通过参照上文的demo,我们结合源码分析在简单的API调用的背后,到底使用了什么技巧来实现各个模块的依赖加载以及模块API的导出。

模块类和状态类

首先定义了一个Module类,对应与一个模块

function Module(uri, deps) {
 this.uri = uri
 this.dependencies = deps || []
 this.exports = null
 this.status = 0

 // Who depends on me
 this._waitings = {}

 // The number of unloaded dependencies
 this._remain = 0
}

Module有一些属性,uri对应该模块的绝对url,在Module.define函数中会有介绍;dependencies为依赖模块数组;exports为导出的API;status为当前的状态码;_waitings对象为当前依赖该模块的其他模块哈希表,其中key为其他模块的url;_remain为计数器,记录还未加载的模块个数。

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
}

上述为状态对象,记录模块的当前状态:模块初始化状态为0,当加载该模块时,为状态fetching;模块加载完毕并且缓存在cacheMods后,为状态saved;loading状态意味着正在加载该模块的其他依赖模块;loaded表示所有依赖模块加载完毕,执行该模块的回调函数,并设置依赖该模块的其他模块是否还有依赖模块未加载,若加载完毕执行回调函数;executing状态表示该模块正在执行;executed则是执行完毕,可以使用exports的API。

模块的定义

commonJS规范规定用define函数来定义一个模块。define可以接受1,2,3个参数均可,不过对于Module/wrappings规范而言,module.declare或者define函数只能接受一个参数,即工厂函数或者对象。不过原则上接受参数的个数并没有本质上的区别,只不过库在后台给额外添加模块名。

seajs鼓励使用define(function(require,exports,module){})这种模块定义方式,这是典型的Module/wrappings规范实现。但是在后台通过解析工厂函数的require方法来获取依赖模块并给模块设置id和url。

// Define a module
Module.define = function (id, deps, factory) {
 var argsLen = arguments.length

 // define(factory)
 if (argsLen === 1) {
 factory = id
 id = undefined
 }
 else if (argsLen === 2) {
 factory = deps

 // define(deps, factory)
 if (isArray(id)) {
 deps = id
 id = undefined
 }
 // define(id, factory)
 else {
 deps = undefined
 }
 }

 // Parse dependencies according to the module factory code
 // 如果deps为非数组,则序列化工厂函数获取入参。
 if (!isArray(deps) && isFunction(factory)) {
 deps = parseDependencies(factory.toString())
 }

 var meta = {
 id: id,
 uri: Module.resolve(id), // 绝对url
 deps: deps,
 factory: factory
 }

 // Try to derive uri in IE6-9 for anonymous modules
 // 导出匿名模块的uri
 if (!meta.uri && doc.attachEvent) {
 var script = getCurrentScript()

 if (script) {
 meta.uri = script.src
 }

 // NOTE: If the id-deriving methods above is failed, then falls back
 // to use onload event to get the uri
 }

 // Emit `define` event, used in nocache plugin, seajs node version etc
 emit("define", meta)

 meta.uri ? Module.save(meta.uri, meta) :
 // Save information for "saving" work in the script onload event
 anonymousMeta = meta
}

模块定义的最后,通过Module.save方法,将模块保存到cachedMods缓存体中。

parseDependencies方法比较巧妙的获取依赖模块。他通过函数的字符串表示,使用正则来获取require(“…”)中的模块名。

var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g
var SLASH_RE = /\\\\/g

function parseDependencies(code) {
 var ret = []
 // 此处使用函数序列化(传入的factory)进行字符串匹配,寻找require(“...”)的关键字
 code.replace(SLASH_RE, "")
 .replace(REQUIRE_RE, function(m, m1, m2) {
 if (m2) {
  ret.push(m2)
 }
 })

 return ret
}

异步加载模块

加载模块可以有多种方式,xhr方式可以同步加载,也可以异步加载,但是存在同源问题,因此难以在此使用。另外script tag方式在IE和现代浏览器下可以保证并行加载和顺序执行,script element方式也可以保证并行加载但不保证顺序执行,因此这两种方式都可以使用。

在seajs中,是采用script element方式来并行加载js/css资源的,并针对旧版本的webkit浏览器加载css做了hack。

function request(url, callback, charset) {
 var isCSS = IS_CSS_RE.test(url)
 var node = doc.createElement(isCSS ? "link" : "script")

 if (charset) {
 var cs = isFunction(charset) ? charset(url) : charset
 if (cs) {
 node.charset = cs
 }
 }

 // 添加 onload 函数。
 addOnload(node, callback, isCSS, url)

 if (isCSS) {
 node.rel = "stylesheet"
 node.href = url
 }
 else {
 node.async = true
 node.src = url
 }

 // For some cache cases in IE 6-8, the script executes IMMEDIATELY after
 // the end of the insert execution, so use `currentlyAddingScript` to
 // hold current node, for deriving url in `define` call
 currentlyAddingScript = node

 // ref: #185 & http://dev.jquery.com/ticket/2709
 baseElement ?
 head.insertBefore(node, baseElement) :
 head.appendChild(node)

 currentlyAddingScript = null
}

function addOnload(node, callback, isCSS, url) {
 var supportOnload = "onload" in node

 // for Old WebKit and Old Firefox
 if (isCSS && (isOldWebKit || !supportOnload)) {
 setTimeout(function() {
 pollCss(node, callback)
 }, 1) // Begin after node insertion
 return
 }

 if (supportOnload) {
 node.onload = onload
 node.onerror = function() {
 emit("error", { uri: url, node: node })
 onload()
 }
 }
 else {
 node.onreadystatechange = function() {
 if (/loaded|complete/.test(node.readyState)) {
 onload()
 }
 }
 }

 function onload() {
 // Ensure only run once and handle memory leak in IE
 node.onload = node.onerror = node.onreadystatechange = null

 // Remove the script to reduce memory leak
 if (!isCSS && !data.debug) {
 head.removeChild(node)
 }

 // Dereference the node
 node = null

 callback()
 }
}
// 针对 旧webkit和不支持onload的CSS节点判断加载完毕的方法
function pollCss(node, callback) {
 var sheet = node.sheet
 var isLoaded

 // for WebKit < 536
 if (isOldWebKit) {
 if (sheet) {
 isLoaded = true
 }
 }
 // for Firefox < 9.0
 else if (sheet) {
 try {
 if (sheet.cssRules) {
 isLoaded = true
 }
 } catch (ex) {
 // The value of `ex.name` is changed from "NS_ERROR_DOM_SECURITY_ERR"
 // to "SecurityError" since Firefox 13.0. But Firefox is less than 9.0
 // in here, So it is ok to just rely on "NS_ERROR_DOM_SECURITY_ERR"
 if (ex.name === "NS_ERROR_DOM_SECURITY_ERR") {
 isLoaded = true
 }
 }
 }

 setTimeout(function() {
 if (isLoaded) {
 // Place callback here to give time for style rendering
 callback()
 }
 else {
 pollCss(node, callback)
 }
 }, 20)
}

其中有些细节还需注意,当采用script element方法插入script节点时,尽量作为首个子节点插入到head中,这是由于一个难以发现的bug:

GLOBALEVAL WORKS INCORRECTLY IN IE6 IF THE CURRENT PAGE HAS <BASE HREF> TAG IN THE HEAD

fetch模块

初始化Module对象时,状态为0,该对象对应的js文件并未加载,若要加载js文件,需要使用上节提到的request方法,但是也不可能仅仅加载该文件,还需要设置module对象的状态及其加载module依赖的其他模块。

这些逻辑在fetch方法中得以体现:

// Fetch a module
// 加载该模块,fetch函数中调用了seajs.request函数
Module.prototype.fetch = function(requestCache) {
 var mod = this
 var uri = mod.uri

 mod.status = STATUS.FETCHING

 // Emit `fetch` event for plugins such as combo plugin
 var emitData = { uri: uri }
 emit("fetch", emitData)
 var requestUri = emitData.requestUri || uri

 // Empty uri or a non-CMD module
 if (!requestUri || fetchedList[requestUri]) {
 mod.load()
 return
 }

 if (fetchingList[requestUri]) {
 callbackList[requestUri].push(mod)
 return
 }

 fetchingList[requestUri] = true
 callbackList[requestUri] = [mod]

 // Emit `request` event for plugins such as text plugin
 emit("request", emitData = {
 uri: uri,
 requestUri: requestUri,
 onRequest: onRequest,
 charset: data.charset
 })

 if (!emitData.requested) {
 requestCache ?
 requestCache[emitData.requestUri] = sendRequest :
 sendRequest()
 }

 function sendRequest() {
 seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset)
 }
 // 回调函数
 function onRequest() {
 delete fetchingList[requestUri]
 fetchedList[requestUri] = true

 // Save meta data of anonymous module
 if (anonymousMeta) {
 Module.save(uri, anonymousMeta)
 anonymousMeta = null
 }

 // Call callbacks
 var m, mods = callbackList[requestUri]
 delete callbackList[requestUri]
 while ((m = mods.shift())) m.load()
 }
}

其中seajs.request就是上节的request方法。onRequest作为回调函数,作用是加载该模块的其他依赖模块。

总结

以上就是seajs模块的依赖加载及模块API的导出的全部内容了,小编会在下一节,将介绍模块之间依赖的加载以及模块的执行。感兴趣的朋友们可以继续关注三水点靠木。

Javascript 相关文章推荐
js下用eval生成JSON对象
Sep 17 Javascript
C#中TrimStart,TrimEnd,Trim在javascript上的实现
Jan 17 Javascript
JavaScript中使用Substring删除字符串最后一个字符
Nov 03 Javascript
深入理解JavaScript是如何实现继承的
Dec 12 Javascript
javascript中this指向详解
Apr 23 Javascript
深入理解jQuery layui分页控件的使用
Aug 17 Javascript
jQuery插件FusionCharts实现的Marimekko图效果示例【附demo源码】
Mar 24 jQuery
javascript数组去重常用方法实例分析
Apr 11 Javascript
node中的session的具体使用
Sep 14 Javascript
vuejs中父子组件之间通信方法实例详解
Jan 17 Javascript
Openlayers3实现车辆轨迹回放功能
Sep 29 Javascript
vue 表单输入框不支持focus及blur事件的解决方案
Nov 17 Vue.js
Angular表单验证实例详解
Oct 20 #Javascript
NODE.JS跨域问题的完美解决方案
Oct 20 #Javascript
seajs学习教程之基础篇
Oct 20 #Javascript
Angular.JS学习之依赖注入$injector详析
Oct 20 #Javascript
Javascript中内建函数reduce的应用详解
Oct 20 #Javascript
基于AngularJS前端云组件最佳实践
Oct 20 #Javascript
分享javascript、jquery实用代码段
Oct 20 #Javascript
You might like
计算一段日期内的周末天数的php代码(星期六,星期日总和)
2009/11/12 PHP
解密ThinkPHP3.1.2版本之模块和操作映射
2014/06/19 PHP
ThinkPHP模板中数组循环实例
2014/10/30 PHP
基于jquery实现的上传图片及图片大小验证、图片预览效果代码
2011/04/12 Javascript
jquery等宽输出文字插件使用介绍
2013/09/18 Javascript
js字符串日期yyyy-MM-dd转化为date示例代码
2014/03/06 Javascript
JS 在指定数组中随机取出N个不重复的数据
2014/06/10 Javascript
JS自定义对象实现Java中Map对象功能的方法
2015/01/20 Javascript
jQuery调用ajax请求的常见方法汇总
2015/03/24 Javascript
AngularJS 自定义过滤器详解及实例代码
2016/09/14 Javascript
Javascript设计模式之装饰者模式详解篇
2017/01/17 Javascript
php输出全部gb2312编码内的汉字方法
2017/03/04 Javascript
Vue input控件通过value绑定动态属性及修饰符的方法
2017/05/03 Javascript
vue-cli之router基本使用方法详解
2017/10/17 Javascript
安装vue-cli报错 -4058 的解决方法
2017/10/19 Javascript
Node.js实现简单的爬取的示例代码
2019/06/25 Javascript
JavaScript 空间坐标的使用
2020/08/19 Javascript
Python模块学习 datetime介绍
2012/08/27 Python
Python3使用requests发闪存的方法
2016/05/11 Python
python 对dataframe下面的值进行大规模赋值方法
2018/06/09 Python
python3中zip()函数使用详解
2018/06/29 Python
python判断数字是否是超级素数幂
2018/09/27 Python
django框架使用orm实现批量更新数据的方法
2019/06/21 Python
python模拟实现斗地主发牌
2020/01/07 Python
css3实现input输入框颜色渐变发光效果代码
2014/04/02 HTML / CSS
全球地下的服装和态度:Slam Jam
2018/02/04 全球购物
JD Sports芬兰:英国领先的运动鞋和运动服饰零售商
2018/11/16 全球购物
波兰多品牌运动商店:StreetStyle24.pl
2020/09/22 全球购物
新闻记者实习自我鉴定
2013/09/19 职场文书
客户经理岗位职责
2013/12/08 职场文书
小饰品店的创业计划书范文
2013/12/28 职场文书
工作鉴定评语
2014/05/04 职场文书
节能减耗标语
2014/06/21 职场文书
民事诉讼答辩状范文
2015/05/21 职场文书
中国汉字听写大会观后感
2015/06/02 职场文书
Python实战之疫苗研发情况可视化
2021/05/18 Python