Node.js从 RequireJs 源码剖析脚本加载原理

 引言

  俗话说的好,不欣赏钻研规律的程序员不是好的程序员,不希罕读源码的程序员不是好的
jser。那两日看到了有关前端模块化的题材,才发觉 JavaScript
社区为了前端工程化真是苦思冥想。先天研究了一天前端模块化的难题,先是大约理解了下模块化的标准规范,然后了然了一晃
RequireJs 的语法和行使办法,最终探讨了下 RequireJs
的设计形式和源码,所以想记录一下连锁的体验,剖析一下模块加载的规律。

 一、认识 RequireJs

   在上马从前,大家需求精通前端模块化,本文不研讨关于前端模块化的题目,有关那地方的难点得以参考阮一峰的接2连3串文章 Javascript
模块化编程

  使用 RequireJs
的率先步:前往官网 http://requirejs.org/

  第叁步:下载文件;

  Node.js 1

  Node.js 2

 

   第3步:在页面中引进 requirejs.js
并设置 main 函数;

1 <script type="text/javascript" src="scripts/require.js" data-main="scripts/main.js"></script>

  然后我们就足以在 main.js
文件里编制程序了,requirejs 采取了 main
函数式的思维,1个文书即为1个模块,模块与模块之间能够重视,也能够绝不关系。使用
requirejs
,我们在编制程序时就不自然全部模块都引进页面,而是需求三个模块,引进3个模块,就一定于
Java 在那之中的 import 一样。

  定义模块:

 1 //直接定义一个对象
 2 define({
 3     color: "black",
 4     size: "unisize"
 5 });
 6 //通过函数返回一个对象,即可以实现 IIFE
 7 define(function () {
 8     //Do setup work here
 9 
10     return {
11         color: "black",
12         size: "unisize"
13     }
14 });
15 //定义有依赖项的模块
16 define(["./cart", "./inventory"], function(cart, inventory) {
17         //return an object to define the "my/shirt" module.
18         return {
19             color: "blue",
20             size: "large",
21             addToCart: function() {
22                 inventory.decrement(this);
23                 cart.add(this);
24             }
25         }
26     }
27 );

  导入模块:

1 //导入一个模块
2 require(['foo'], function(foo) {
3     //do something
4 });
5 //导入多个模块
6 require(['foo', 'bar'], function(foo, bar) {
7     //do something
8 });

  关于 requirejs 的选拔,能够查阅官网
API ,也得以参考 RequireJS 和 AMD
规范
 ,本文暂不对 requirejs
的选取实行教学。

 2、main 函数入口

  requirejs
的核情绪想之壹正是行使1个鲜明的函数入口,就像是 C++ 的 int main(),Java
的 public static void main(),requirejs 的行使方法是把 main 函数缓存在
script 标签上。约等于将脚本文件的 url 缓存在 script 标签上。

1 <script type="text/javascript" src="scripts/require.js" data-main="scripts/main.js"></script>

  初来乍到计算机同学壹看,哇!script
标签难道还有啥样无人问津的属性吗?吓得本人飞快打开了 W3C 查看相关
API,并为本身的 HTML 基础知识感到惭愧,然则遗憾的是 script
标签并未有关的习性,甚至那都不是三个行业内部的个性,那么它究竟是怎么玩意儿呢?上边直接上有些requirejs 源码:

1 //Look for a data-main attribute to set main script for the page
2 //to load. If it is there, the path to data main becomes the
3 //baseUrl, if it is not already set.
4 dataMain = script.getAttribute('data-main');

  实际上在 requirejs 中只是收获在 script
标签上缓存的数量,然后取出数据加载而已,也正是跟动态加载脚本是同样的,具体是怎么操作,在上面的讲授中会放出源码。

 3、动态加载脚本

  那一有的是全方位 requirejs
的中坚,我们领会在 Node.js
中加载模块的艺术是壹道的,这是因为在劳动器端全数文件都存款和储蓄在本土的硬盘上,传输速率快并且安静。而换做了浏览器端,就不能够如此干了,因为浏览器加载脚本会与服务器进行通讯,那是一个不敢问津的呼吁,若是应用同步的办法加载,就只怕会间接不通下去。为了防备浏览器的不通,咱们要运用异步的主意加载脚本。因为是异步加载,所以与模块相依赖的操作就非得得在本子加载成功后实施,那里就得使用回调函数的款型。

  我们精通,假使呈现的在 HTML
中定义脚本文件,那么脚本的履行顺序是同步的,比如:

1 //module1.js
2 console.log("module1");

1 //module2.js
2 console.log("module2");

1 //module3.js
2 console.log("module3");

1 <script type="text/javascript" src="scripts/module/module1.js"></script>
2 <script type="text/javascript" src="scripts/module/module2.js"></script>
3 <script type="text/javascript" src="scripts/module/module3.js"></script>

  那么在浏览器端总是会输出:

Node.js 3

  可是倘如若动态加载脚本的话,脚本的履行各样是异步的,而且不光是异步的,照旧冬季的:

 1 //main.js
 2 console.log("main start");
 3 
 4 var script1 = document.createElement("script");
 5 script1.src = "scripts/module/module1.js";
 6 document.head.appendChild(script1);
 7 
 8 var script2 = document.createElement("script");
 9 script2.src = "scripts/module/module2.js";
10 document.head.appendChild(script2);
11 
12 var script3 = document.createElement("script");
13 script3.src = "scripts/module/module3.js";
14 document.head.appendChild(script3);
15 
16 console.log("main end");

   使用那种格局加载脚本会造成脚本的冬季加载,浏览器遵照先来先运营的章程执行脚本,要是module壹.js 文书相比大,那么最棒有相当大概率会在 module2.js 和 module三.js
后实施,所以说那也是不可控的。要通晓一个顺序当中最大的 BUG
正是贰个不可控的 BUG
,有时候它恐怕按顺序执行,有时候它也许乱序,那终将不是大家想要的。

Node.js 4

  注意那里的还有二个主假设,”module” 的出口永远会在 “main end”
之后。那多亏动态加载脚本异步性的特色,因为脚下的脚本是一个 task
,而随便任何脚本的加载速度有多快,它都会在 Event Queue
的后面伺机调度执行。那里提到到一个最首要的文化 — 伊夫nt Loop ,倘使你还对
JavaScript 伊夫nt Loop 不打听,那么请先读书那篇小说 Node.js,深深了然JavaScript 事件循环(一)— 伊芙nt
Loop

 4、导入模块原理

  在上一小节,大家理解到,使用动态加载脚本的格局会使脚本冬天执行,那终将是软件开发的梦魇,想象一下您的模块之间存在上下正视的涉及,而那时他们的加载顺序是不可控的。动态加载同时也颇具异步性,所以在
main.js 脚本文件中根本无法访问到模块文件中的任何变量。那么 requirejs
是怎么着化解这些标题标吗?大家掌握在 requirejs
中,任何公文都是一个模块,八个模块相当于一个文件,包蕴主模块
main.js,上面我们看一段 requirejs 的源码:

 1 /**
 2  * Creates the node for the load command. Only used in browser envs.
 3  */
 4 req.createNode = function (config, moduleName, url) {
 5     var node = config.xhtml ?
 6             document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
 7             document.createElement('script');
 8     node.type = config.scriptType || 'text/javascript';
 9     node.charset = 'utf-8';
10     node.async = true;
11     return node;
12 };

  在那段代码中大家得以见到, requirejs
导入模块的章程实际正是创造脚本标签,壹切的模块都亟需经过那么些办法创造。那么
requirejs
又是何等处理异步加载的啊?传说江湖上高高的深的医道不是什么灵丹妙药,而是以毒攻毒,requirejs
也深得其精华,既然动态加载是异步的,那么自个儿也用异步来对付你,使用 onload
事件来处理回调函数:

 1 //In the browser so use a script tag
 2 node = req.createNode(config, moduleName, url);
 3 
 4 node.setAttribute('data-requirecontext', context.contextName);
 5 node.setAttribute('data-requiremodule', moduleName);
 6 
 7 //Set up load listener. Test attachEvent first because IE9 has
 8 //a subtle issue in its addEventListener and script onload firings
 9 //that do not match the behavior of all other browsers with
10 //addEventListener support, which fire the onload event for a
11 //script right after the script execution. See:
12 //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
13 //UNFORTUNATELY Opera implements attachEvent but does not follow the script
14 //script execution mode.
15 if (node.attachEvent &&
16     //Check if node.attachEvent is artificially added by custom script or
17     //natively supported by browser
18     //read https://github.com/requirejs/requirejs/issues/187
19     //if we can NOT find [native code] then it must NOT natively supported.
20     //in IE8, node.attachEvent does not have toString()
21     //Note the test for "[native code" with no closing brace, see:
22     //https://github.com/requirejs/requirejs/issues/273
23     !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
24     !isOpera) {
25     //Probably IE. IE (at least 6-8) do not fire
26     //script onload right after executing the script, so
27     //we cannot tie the anonymous define call to a name.
28     //However, IE reports the script as being in 'interactive'
29     //readyState at the time of the define call.
30     useInteractive = true;
31 
32     node.attachEvent('onreadystatechange', context.onScriptLoad);
33     //It would be great to add an error handler here to catch
34     //404s in IE9+. However, onreadystatechange will fire before
35     //the error handler, so that does not help. If addEventListener
36     //is used, then IE will fire error before load, but we cannot
37     //use that pathway given the connect.microsoft.com issue
38     //mentioned above about not doing the 'script execute,
39     //then fire the script load event listener before execute
40     //next script' that other browsers do.
41     //Best hope: IE10 fixes the issues,
42     //and then destroys all installs of IE 6-9.
43     //node.attachEvent('onerror', context.onScriptError);
44 } else {
45     node.addEventListener('load', context.onScriptLoad, false);
46     node.addEventListener('error', context.onScriptError, false);
47 }
48 node.src = url;

  注目的在于那段源码个中的监听事件,既然动态加载脚本是异步的的,那么干脆使用
onload
事件来拍卖回调函数,那样就确定保障了在大家的程序执行前借助的模块一定会提早加载成功。因为在事件队列里,
onload
事件是在本子加载成功之后触发的,也正是在事件队列里面永远处于信赖模块的前边,例如大家履行:

1 require(["module"], function (module) {
2     //do something
3 });

  那么在事件队列里面包车型地铁周旋顺序会是如此:

Node.js 5

  相信细心的同校可能会专注到了,在源码个中不光光有
onload 事件,同时还添加了2个  onerror 事件,大家在应用 requirejs
的时候也足以定义3个模块加载失利的处理函数,那几个函数在底层也就对应了
onerror 事件。同理,其和 onload
事件相同是三个异步的风云,同时也永远发生在模块加载之后。

  聊到此地 requirejs
的主干模块思想也就一目了解了,可是里面的经过还远不直这几个,博主只是将模块加载的完成思想抛了出去,但
requirejs
的有血有肉落实还要复杂的多,比如大家定义模块的时候能够导入重视模块,导入模块的时候还足以导入七个依靠,具体的达成格局本身就从未探索过了,
requirejs 固然十分的小,可是源码也是有3000多行的…
…可是只要知道了动态加载脚本的原理过后,其构思也就简单明白了,比如本身将来就能够想到二个大约的兑现四个模块信赖的不2诀要,使用计数的不二等秘书诀检查模块是还是不是加载完全:

 1 function myRequire(deps, callback){
 2     //记录模块加载数量
 3     var ready = 0;
 4     //创建脚本标签
 5     function load (url) {
 6         var script = document.createElement("script");
 7         script.type = 'text/javascript';
 8         script.async = true;
 9         script.src = url;
10         return script;
11     }
12     var nodes = [];
13     for (var i = deps.length - 1; i >= 0; i--) {
14         nodes.push(load(deps[i]));
15     }
16     //加载脚本
17     for (var i = nodes.length - 1; i >= 0; i--) {
18         nodes[i].addEventListener("load", function(event){
19             ready++;
20             //如果所有依赖脚本加载完成,则执行回调函数;
21             if(ready === nodes.length){
22                 callback()
23             }
24         }, false);
25         document.head.appendChild(nodes[i]);
26     }
27 }

  实验一下是否能够工作:

1 myRequire(["module/module1.js", "module/module2.js", "module/module3.js"], function(){
2     console.log("ready!");
3 });

 Node.js 6

  Yes, it’s work!

 总结

  requirejs 加载模块的核激情想是采纳了动态加载脚本的异步性以及 onload
事件以毒攻毒,关于脚本的加载,大家供给留意一下几点:

  •   在 HTML 中引进 <script> 标签是同步加载;
  •   在剧本中动态加载是异步加载,且由于被加载的台本在事件队列的后端,由此接连会在此时此刻剧本之后执行;
  •   使用 onload 和 onerror
    事件能够监听脚本加载成功,以异步的轩然大波来拍卖异步的轩然大波;

 参考文献:

  阮一峰 — RequireJS 和 AMD
规范

  阮一峰 — Javascript
模块化编制程序

  requirejs.org — requirejs api