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 相关文章推荐
文本框回车提交与禁止提交示例
Sep 27 Javascript
JavaScript获取路径设计源码
May 22 Javascript
JavaScript中九种常用排序算法
Sep 02 Javascript
浅谈JavaScript实现面向对象中的类
Dec 09 Javascript
原生JS下拉加载插件分享
Dec 26 Javascript
基于vue.js路由参数的实例讲解——简单易懂
Sep 07 Javascript
MUI顶部选项卡的用法(tab-top-webview-main)详解
Oct 08 Javascript
React Native验证码倒计时工具类分享
Oct 24 Javascript
详解JavaScript中的强制类型转换
Apr 15 Javascript
vue组件命名和props命名代码详解
Sep 01 Javascript
如何实现echarts markline标签名显示自己想要的
Jul 20 Javascript
JS实现页面侧边栏效果探究
Jan 08 Javascript
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和MySQL保存和输出图片
2006/10/09 PHP
php mssql 时间格式问题
2009/01/13 PHP
phpmailer在服务器上不能正常发送邮件的解决办法
2014/07/08 PHP
PHP 以POST方式提交XML、获取XML,解析XML详解及实例
2016/10/26 PHP
PHP使用第三方即时获取物流动态实例详解
2017/04/27 PHP
基于jQuery的左右滚动实现代码
2010/12/03 Javascript
采用自执行的匿名函数解决for循环使用闭包的问题
2014/09/11 Javascript
jQuery中has()方法用法实例
2015/01/06 Javascript
JavaScript中通过prototype属性共享属性和方法的技巧实例
2015/03/13 Javascript
微信小程序  wx.request合法域名配置详解
2016/11/23 Javascript
基于javascript实现按圆形排列DIV元素(三)
2016/12/02 Javascript
关于Vue.js一些问题和思考学习笔记(2)
2016/12/02 Javascript
javascript+html5+css3自定义提示窗口
2017/06/21 Javascript
基于 webpack2 实现的多入口项目脚手架详解
2017/06/26 Javascript
JS+Ajax实现百度智能搜索框
2017/08/04 Javascript
解决Vue 浏览器后退无法触发beforeRouteLeave的问题
2017/12/24 Javascript
详解Vue webapp项目通过HBulider打包原生APP
2018/06/29 Javascript
mpvue跳转页面及注意事项
2018/08/03 Javascript
小程序关于请求同步的总结
2019/05/05 Javascript
Vue仿微信app页面跳转动画效果
2019/08/21 Javascript
解决layui 表单元素radio不显示渲染的问题
2019/09/04 Javascript
vue实现跳转接口push 转场动画示例
2019/11/01 Javascript
JS对日期操作封装代码实例
2019/11/08 Javascript
Javascript前端下载后台传来的文件流代码实例
2020/08/18 Javascript
javascript实现固定侧边栏
2021/02/09 Javascript
python读取注册表中值的方法
2013/04/08 Python
Python补齐字符串长度的实例
2018/11/15 Python
PyTorch使用cpu加载模型运算方式
2020/01/13 Python
python实现在列表中查找某个元素的下标示例
2020/11/16 Python
中国跨境海淘网站:考拉海购
2016/08/01 全球购物
物业管理求职自荐信
2013/09/25 职场文书
机械专业毕业生自荐信
2013/11/02 职场文书
交通事故私了协议书
2014/04/16 职场文书
2014年秋季开学典礼致辞
2014/08/02 职场文书
文艺委员竞选稿
2015/11/19 职场文书
Django框架之路由用法
2022/06/10 Python