《JavaScript框架设计》——2.3 require方法
本节书摘来自异步社区《JavaScript框架设计》一书中的第2章,第2.3节,作者:司徒正美著,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.3 require方法require方法的作用是当依赖列表都加载完毕,执行用户回调。因此这里有个加载的过程,整个加载过程分为以下几步。
(1)取得依赖列表的第一个ID,转换为URL。无论是通过basePath+ID+".js",还是以映射的方式直接得到。
(2)检测此模块有没有加载过,或正在被加载。因此我们需要一个对象来保持所有模块的加载情况。当用户从来没有加载过此节点时,就进入加载流程。
(3)创建script节点,绑定onerror、onload、onreadychange等事件判定加载成功与否,然后添加href并插入DOM树,开始加载。
(4)将模块的URL,依赖列表等构建成一个对象,放到检测队列中,在上面的事件触发时进行检测。
在mass的加载器,它支持允许第一个参数为字符串,然后内部按空格或逗号切分为ID数组,以及做去重处理,其他都一样。
mass在这基础上做了扩展。
(1)模块ID本来就是URL的简体,因此可以包含斜线(/),并以/划分为多项。
(2)模块ID应该是以符合变量的规则的字符串组成,第一个项可以是“.”或“..”。这些都是URL的基本的规则,表示当前目录与父目录。
(3)模块ID的未尾可以包含“.js”,但如果它是指向一个CSS文件,那么必须以CSS结尾。
(4)如果以“/”或“./”开头,表示它与加载它的模块在同一目录。
(5)如果以“..”开头,表示它在加载它的模块的上一级目录,如果存在多个“..”,就要向上找。
(6)如果模块ID是“mass”,不做转换与加载,这表示mass框架的种子模块,也就是加载器的所在模块。
(7)如果模块ID是“ready”,不做转换与加载,这用于延迟用户回调到DOM树建完后执行。避免出现domReady的回调函数与模块的回调函数出现套嵌。
(8)如果情况就直接在前面按上basePath——加载器所在的目录!
除了这些情况外,我们通常还用到映射,就是允许用户在事前用一个方法,把ID与完整的URL对应好,这样就直接拿。mass称之为别名机制。ID只是给用户用的,框架还是URL做加载或其他检测。此外,AMD还发展一种shim技术,shim就是垫片的意思,目的是让不符合AMD定义的JavaScript文件也能无缝切入我们的加载器系统。
如这个是普通的别名机制:
require.config({ alias: { lang: http://common.cnblogs.com/script/mass/lang.js, css: http://common.cnblogs.com/script/mass/css.js
而对于jQuery或其插件,我们需要shim机制:
require.config({ alias: { jquery: { src: http://common.cnblogs.com/scriptjquery.js, exports: "$" jquery.tooltip: { src: http://common.cnblogs.com/script/ui/tooltip.js, exports: "$", deps: ["jquery"]
下面是require的源码:
window.require = $.require = function(list, factory, parent) { // 用于检测它的依赖是否都为2 var deps = {}, // 用于保存依赖模块的返回值 args = [], // 需要安装的模块数 dn = 0, // 已安装完的模块数 cn = 0, id = parent || "callback" + setTimeout("1"); parent = parent || basepath;//basepath为加载器的路径 String(list).replace($.rword, function(el) { var url = loadJSCSS(el, parent) if (url) { dn++; if (modules[url] modules[url].state === 2) { cn++; if (!deps[url]) { args.push(url); deps[url] = "司徒正美"; //去重 modules[id] = {//创建一个对象,记录模块的加载情况与其他信息 id: id, factory: factory, deps: deps, args: args, state: 1 if (dn === cn) { //如果需要安装的等于已安装好的 fireFactory(id, args, factory); //安装到框架中 } else { //放到检测队列中,等待checkDeps处理 loadings.unshift(id); checkDeps();
每require一次,相当于把当前的用户回调当成一个不用加载的匿名模块,ID是随机生成,回调是否执行,要待到deps对象里面所有值都为2。
require里有三个重要方法:loadJSCSS,它用于转换ID为URL,后面再调用loadJS, loadCSS,或再调用require方法;fireFactory,就是执行用户回调,我们的最终目的;checkDeps,检测依赖是否都安装好,安装好就执行fireFactory。
function loadJSCSS(url, parent, ret, shim) { //1. 特别处理mass|ready标识符 if (/^(mass|ready)$/.test(url)) { return url; //2. 转化为完整路径 if ($.config.alias[url]) {//别名机制 ret = $.config.alias[url]; if (typeof ret === "object") { shim = ret; ret = ret.src; } else { if (/^(\w+)(\d)?:.*/.test(url)) { //如果本来就是完整路径 ret = url; } else { parent = parent.substr(0, parent.lastIndexOf(/)); var tmp = url.charAt(0); if (tmp !== "." tmp !== "/") { //相对于根路径 ret = basepath + url; } else if (url.slice(0, 2) === "./") { //相对于兄弟路径 ret = parent + url.slice(1); } else if (url.slice(0, 2) === "..") { //相对于父路径 var arr = parent.replace(/\/$/, "").split("/"); tmp = url.replace(/\.\.\//g, function() { arr.pop(); return ""; ret = arr.join("/") + "/" + tmp; } else if (tmp === "/") { ret = parent + url;//相对于兄弟路径 } else { $.error("不符合模块标识规则: " + url); var src = ret.replace(/[?#].*/, ""), ext; if (/\.(css|js)$/.test(src)) { ext = RegExp.$1; if (!ext) { //如果没有后缀名,加上后缀名 src += ".js"; ext = "js"; //3. 开始加载JS或CSS if (ext === "js") { if (!modules[src]) { //如果之前没有加载过 modules[src] = { id: src, parent: parent, exports: {} if (shim) {//shim机制 require(shim.deps || "", function() { loadJS(src, function() { modules[src].state = 2; modules[src].exports = typeof shim.exports === "function" ? shim.exports() : window[shim.exports]; checkDeps(); } else { loadJS(src); return src; } else { loadCSS(src);
注意,上面的modules[src]是以完整路径做ID的,它对应的对象没有state属性,表示其正在加载中。
loadJS与loadCSS方法就比较纯粹了,不过loadJS会做个死链检测checkFail。
function loadJS(url, callback) { //通过script节点加载目标模块 var node = DOC.createElement("script"); node.className = moduleClass; //让getCurrentScript只处理类名为moduleClass的script节点 node[W3C ? "onload" : "onreadystatechange"] = function() { if (W3C || /loaded|complete/i.test(node.readyState)) { //factorys里面装着define方法的工厂函数(define(id?,deps?, factory)) var factory = factorys.pop(); factory factory.delay(node.src); if (callback) { callback(); if (checkFail(node, false, !W3C)) { $.log("已成功加载 " + node.src, 7); node.onerror = function() { checkFail(node, true); //插入到head的第一个节点前,防止IE6下head标签没闭合前使用appendChild抛错 node.src = url; head.insertBefore(node, head.firstChild);
checkFail方法主要是用于开发调试。有3个参数。node——script节点,onError——是否为onerror触发,fuckIE——对应旧版本IE的hack。思路是,JavaScript文件从加载到解析到执行需要一个过程,在interact阶段,我们的JavaScript代码已经有些部分可以执行了,这时我们将模块对象的state改为1,如果还是undefined,我们就可识别它为死链。不过,此hack对不是以AMD定义的JavaScript模块无效,因为将state改1的逻辑是由define方法执行的。如果判定是死链,我们就把此节点移除。
function checkFail(node, onError, fuckIE) { var id = node.src;//检测是否死链 node.onload = node.onreadystatechange = node.onerror = null; if (onError || (fuckIE !modules[id].state)) { setTimeout(function() { head.removeChild(node); $.log("加载 " + id + " 失败" + onError + " " + (!modules[id].state), 7); } else { return true;
checkDeps方法会在用户加载模块之前及script.onload后各执行一次,检测模块的依赖情况,如果模块没有任何依赖或state都为2了,我们调用fireFactory方法。
function checkDeps() { loop: for (var i = loadings.length, id; id = loadings[--i]; ) { var obj = modules[id], deps = obj.deps; for (var key in deps) { if (hasOwn.call(deps, key) modules[key].state !== 2) { continue loop; //如果deps是空对象或者其依赖的模块的状态都是2 if (obj.state !== 2) { loadings.splice(i, 1);//必须先移除再安装,防止在IE下DOM树建完后手动刷新页面,会多次执行它 fireFactory(obj.id, obj.args, obj.factory); checkDeps();//如果成功,则再执行一次,以防有些模块就差本模块没有安装好
历经千辛万苦,我们终于到达fireFactory方法。它的工作是从modules中收集各模块的返回值,执行factory, 完成模块的安装。
function fireFactory(id, deps, factory) { for (var i = 0, array = [], d; d = deps[i++]; ) { array.push(modules[d].exports); var module = Object(modules[id]), ret = factory.apply(global, array); module.state = 2; if (ret !== void 0) { modules[id].exports = ret; return ret; }
js之JavaScript开发者应懂的33个概念 这个项目是为了帮助开发者掌握 JavaScript 概念而创立的。它不是必备,但在未来学习(JavaScript)中,可以作为一篇指南
请点击此处输入图片描述 浏览器正在逐步的支持原生JavaScript模块。Safari和Chrome的最新版本已经支持它们了,Firefox和Edge也将很快推出。
异步社区 异步社区(www.epubit.com)是人民邮电出版社旗下IT专业图书旗舰社区,也是国内领先的IT专业图书社区,致力于优质学习内容的出版和分享,实现了纸书电子书的同步上架,于2015年8月上线运营。公众号【异步图书】,每日赠送异步新书。
相关文章
- JavaScript跨域总结与解决办法
- JavaScript 中改变 this 的指向
- 【JavaScript】JS最简单的二级折叠菜单的实现方法(完整实例)
- JavaScript 21. 调试
- JavaScript - 原生 Ajax 请求封装,支持自定义 headers、同步或异步执行等(附带详细代码注释及使用示例)
- 【Selenium核心技术篇】execute_script方法执行JavaScript操作页面滚动条
- JavaScript-每隔5分钟执行一次ajax请求的实现方法
- 《JavaScript框架设计》——2.4 define方法
- 用javascript写一个emoji表情插件
- JavaScript关闭窗口的同时打开新页面的方法
- JavaScript 数组遍历方法的对比
- JS-安全检测JavaScript基本数据类型和内置对象的方法
- 前端JavaScript小技巧「建议收藏」
- 25个JavaScript数组方法代码示例
- JavaScript 新语法详解:Class 的私有属性与私有方法
- JavaScript机器学习之KNN算法
- 刚看完了一本关于javascript的书感觉受益匪浅,原来不懂的东西这么多,想问问怎么成为大神?求教!!!!!!
- JavaScript Array数组slice方法的使用
- JavaScript之call()和apply()方法详解
- JavaScript引用类型之Array数组的排序方法
- 深入理解JavaScript系列(47):对象创建模式(上篇)
- javascript通过改变滚动条滚动来显示某些元素的scrollIntoView()方法
- JavaScript 数字前补“0”的五种方法