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 相关文章推荐
flash javascript之间的通讯方法小结
Dec 20 Javascript
js网页版计算器的简单实现
Jul 02 Javascript
一个JavaScript获取元素当前高度的实例
Oct 29 Javascript
javacript使用break内层跳出外层循环分析
Jan 12 Javascript
jQuery提示插件alertify使用指南
Apr 21 Javascript
Angular企业级开发——MVC之控制器详解
Feb 20 Javascript
关于Node.js中Buffer的一些你可能不知道的用法
Mar 28 Javascript
Extjs 中的 Treepanel 实现菜单级联选中效果及实例代码
Aug 22 Javascript
Vue.directive使用注意(小结)
Aug 31 Javascript
详解vue中的computed的this指向问题
Dec 05 Javascript
js的Object.assign用法示例分析
Mar 05 Javascript
element-ui中el-upload多文件一次性上传的实现
Dec 02 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时间不正确的解决方法
2008/04/09 PHP
php中将数组转成字符串并保存到数据库中的函数代码
2013/09/29 PHP
php中常量DIRECTORY_SEPARATOR用法深入分析
2014/11/14 PHP
PHP也能干大事之PHP中的编码解码详解
2015/04/20 PHP
php字符串操作常见问题小结
2016/10/11 PHP
JavaScript基础语法让人疑惑的地方小结
2012/05/23 Javascript
js内存泄露的几种情况详细探讨
2013/05/31 Javascript
js+html5实现的自由落体运动效果代码
2016/01/28 Javascript
jq实现左滑显示删除按钮,点击删除实现删除数据功能(推荐)
2016/08/23 Javascript
微信小程序 欢迎页面的制作(源码下载)
2017/01/09 Javascript
node基于async/await对mysql进行封装
2019/06/20 Javascript
react 移动端实现列表左滑删除的示例代码
2019/07/04 Javascript
微信小程序 组件的外部样式externalClasses使用详解
2019/09/06 Javascript
webgl实现物体描边效果的方法介绍
2019/11/27 Javascript
element-ui中dialog弹窗关闭按钮失效的解决
2020/09/22 Javascript
Python制作简单的网页爬虫
2015/11/22 Python
python 捕获shell脚本的输出结果实例
2017/01/04 Python
python3 读取Excel表格中的数据
2018/10/16 Python
django admin.py 外键,反向查询的实例
2019/07/26 Python
Python 操作 PostgreSQL 数据库示例【连接、增删改查等】
2020/04/21 Python
浅谈keras使用预训练模型vgg16分类,损失和准确度不变
2020/07/02 Python
Python3爬虫带上cookie的实例代码
2020/07/28 Python
CSS3 animation实现简易幻灯片轮播特效
2016/09/27 HTML / CSS
HTML5实现预览本地图片
2016/02/17 HTML / CSS
Merrell迈乐澳大利亚网站:购买户外登山鞋
2017/05/28 全球购物
璀璨的珍珠、密钉和个性化珠宝:Lily & Roo
2021/01/21 全球购物
前厅收银主管岗位职责
2014/02/04 职场文书
幼儿园庆六一活动方案
2014/03/06 职场文书
餐饮业员工工作决心书
2014/03/11 职场文书
外语系毕业生求职自荐信
2014/04/12 职场文书
十佳中学生事迹材料
2014/06/02 职场文书
幼儿园食品安全责任书
2015/05/08 职场文书
感谢师恩主题班会
2015/08/17 职场文书
初任公务员培训心得体会
2016/01/08 职场文书
创业计划书之淘宝网店
2019/10/08 职场文书
JS监听Esc 键触发事键
2021/04/14 Javascript