从零开始写JavaScript框架(一)

2019年12月08日 阅读数:116
这篇文章主要向大家介绍从零开始写JavaScript框架(一),主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

1. 模块的定义和加载javascript

1.1 模块的定义

一个框架想要能支撑较大的应用,首先要考虑怎么作模块化。有了内核和模块加载系统,外围的模块就能够一个一个增长。不一样的JavaScript框架,实现模块化方式各有不一样,咱们来选择一种比较优雅的方式做个讲解。html

先问个问题:咱们作模块系统的目的是什么?若是以为这个问题难以回答,能够从反面来考虑:假如不作模块系统,有什么样的坏处?前端

咱们经历过比较粗放、混乱的前端开发阶段,页面里充满了全局变量,全局函数。那时候要复用js文件,就是把某些js函数放到一个文件里,而后让多个页面都来引用。java

考虑到一个页面能够引用多个这样的js,这些js互相又不知作别人里面写了什么,很容易形成命名的冲突,而产生这种冲突的时候,又没有哪里可以提示出来。因此咱们要有一种办法,把做用域比较好地隔开。node

JavaScript这种语言比较奇怪,奇怪在哪里呢,它的现有版本里没package跟class,要是有,咱们也不必来考虑什么本身作模块化了。那它是要用什么东西来隔绝做用域呢?git

在不少传统高级语言里,变量做用域的边界是大括号,在{}里面定义的变量,做用域不会传到外面去,但咱们的JavaScript大人不是这样的,他的边界是function。因此咱们这段代码,i仍然能打出值:github

那么,咱们只能选用function作变量的容器,把每一个模块封装到一个function里。如今问题又来了,这个function自己的做用域是全局的,怎么办?咱们想不到办法,拔剑四顾心茫然。web

咱们有没有什么可参照的东西呢?这时候,脑海中一群语言飘过: C语言飘过:“我不是面向对象语言哦~不须要像你这么组织哦~”,“死开!” Java飘过:“我是纯面向对象语言哦,连main都要在类中哦,编译的时候经过装箱清单指定入口哦~”,“死开!” C++飘过:“我也是纯面向对象语言哦”,等等,C++是纯面向对象的语言吗?你的main是什么???main是特例,不在任何类中!chrome

啊,咱们发现了什么,既然没法避免全局的做用域,那与其让100个function都全局,不如只让一个来全局,其余的都由它管理。shell

原本咱们打算本身当上帝的,如今只好改行先当个工商局长。你想开店吗?先来注册,否则封杀你!因而良民们纷纷来注册。店名叫什么,从哪进货,卖什么的,一一登记在案,为了方便下面的讨论,咱们连进货的过程都让工商局管理起来。

店名,指的就是这里的模块名,从哪里进货,表明它依赖什么其余模块,卖什么,表示它对外提供一些什么特性。

好了,考虑到咱们的这个注册管理机构是个全局做用域,咱们还得把它挂在window上做为属性,而后再用一个function隔离出来,要否则,别人也定义一个同名的,就把咱们覆盖掉了。

在这个module方法内部,应当怎么去实现呢?咱们的module应当有一个地方存储,但存储是要在工商局内部的,不是随便什么人均可以看到的,因此,这个存储结构也放在工商局一样的做用域里。

用什么结构去存储呢?工商局备案的时候,店名不能跟已有的重复,因此咱们发现这是用map的很好场景,考虑到JavaScript语言层面没有map,咱们弄个Object来存。

如今,模块的存储结构就搞好了。

1.2 模块的使用

存的部分搞好了,咱们来看看怎么取。如今来了一个商家,卖木器的,他须要从一个卖钉子的那边进货,卖钉子的已经来注册过了,如今要让这个木器厂能买到钉子。如今的问题是,两个商家处于不一样的做用域,也就是说,它们互相不可见,那经过什么方式,咱们才能让他们产生调用关系呢?

我的解决不了的问题仍是得靠政府,有困难要坚定克服,没有困难就制造困难来克服。如今困难有了,该克服了。商家说,我能不能给你个人进货名单,你帮我查一下它们在哪家店,而后告诉我?这么简单的要求固然一口答应下来,可是采用什么方式传递给你呢?这可犯难了。

咱们参考AngularJS框架,写了一个相似的代码:

看这段代码特别在哪里呢?模块A的定义,毫无特别之处,主要看模块B。它在依赖关系里写了一个字符串的A,而后在工厂方法的形参写了一个真真切切的A类型。嗯?这个有些奇怪啊,你的A类型要怎么传递过来呢?实际上是很简单的,由于咱们声明了依赖项的数组,因此能够从依赖项,挨个获得对应的工厂方法,而后建立实例,传进来。

咱们能够看到,这里面递归获取了依赖项,而后看成参数,用这个模块的工厂方法来实例化了一下。这里咱们多作了一个判断,若是模块工厂已经执行过,就缓存在entity属性上,不须要每次都建立。以此类推,假如一个模块有多个依赖项,也能够用相似的方式写,毫无压力:

注意了,D模块的工厂,实参的名称未必就要是跟依赖项一致,好比,之后咱们代码较多,能够给依赖项和模块名称加命名空间,可能变成这样:

这段代码仍然能够正常运行。咱们来作另一个测试,改变形参的顺序:

试试看,咱们的D打出什么结果呢?结果是”abc”,因此说,模块工厂的实参只跟依赖项的定义有关,跟形参的顺序无关。咱们看到,在AngularJS里面,并不是如此,实参的顺序是跟形参一致的,这是怎么作到的呢?

咱们先离开代码,思考这么一个问题:如何得知函数的形参名数组?对,咱们是能够用func.length获得形参个数,但没法获得每一个形参的变量名,那怎么办呢?

AngularJS使用了一种比较极端的办法,分析了函数的字面量。众所周知,在JavaScript中,任何对象都隐含了toString方法,对于一个函数来讲,它的toString就是本身的实现代码,包含函数签名和注释。下面我贴一下AngularJS里面的这部分代码:

能够看到,这个代码也不长,重点是类型为function的那段,首先去除了注释,而后获取了形参列表字符串,这段正则能获取到两个结果,第一个是全函数的实现,第二个才是真正的形参列表,取第二个出来split,就获得了形参的字符串列表了,而后按照这个顺序再去加载依赖模块,就可让形参列表不对应于依赖项数组了。

AngularJS的这段代码很强大,可是要损耗一些性能,考虑到咱们的框架首要原则是简单,甚至能够为此牺牲一些灵活性,咱们不作这么复杂的事情了。

1.3 模块的加载

到目前为止,咱们能够把多个模块都定义在一个文件中,而后手动引入这个js文件,可是若是一个页面要引用不少个模块,引入工做就变得比较麻烦,好比说,单页应用程序(SPA)通常比较复杂,每每包含数以万计行数的js代码,这些代码至少分布在几十个甚至成百上千的模块中,若是咱们也在主界面就加载它们,载入时间会很是难以接受。但咱们能够这样看:主界面加载的时候,并非用到了全部这些功能,可否先加载那些必须的,而把剩下的放在须要用的时候再去加载?

因此咱们能够考虑万能的AJAX,从服务端获取一个js的内容,而后……,怎么办,你固然说不能eval了,由于听说eval很evil啦,可是它evil在哪里呢?主要是破坏全局做用域啦,怎么怎么,可是若是这些文件里面都是按照咱们规定的模块格式写,好像也没有什么在全局做用域的……,好吧。

算了,咱们仍是用最简单的方式了,就是动态建立script标签,而后设置src,添加到document.head里,而后监听它们的完成事件,作后续操做。真的很简单,由于咱们的框架不须要考虑那么多种状况,不须要AMD,不须要require那么麻烦,用这框架的人必须按照这里的原则写。

因此,说真的咱们这里没那么复杂啦,要是大家想看更详细原理的不如去看这个,解释得比我好哎:http://coolshell.cn/articles/9749.html#jtss-tsina

[补一段,@Franky 大神指出了这篇文章中一些不符合现状的地方,我把它也贴在这里,供读者参考]

不少观点都是 史蒂夫那本老书上的观点. 和那时候同期产生的一些数据和资料…因此显得很多东西说的太想固然了… 譬如script标签的加载和执行会阻塞后面资源的加载和执行之类的.说的过于确定了. 好比chrome7+就开始逐渐改进的 预加载机制 就分 head 里的资源, body里的资源 .两个资源是否跨界三种情形. 不提这些浏览器. 咱们看看ie10也一样改进了 死循环10秒 这后面的图片能被提早加载. 就更不用说其余A级浏览器的丰富的优化策略了. 因此仍是建议博主, 别拿几年前的老资料做为依据.尤为这些数据是用来讲明更新速度像在赛跑同样的各个浏览器了.

关于 defer , 彷佛史蒂夫的老书上是这么说的么? 显然没有测试全非ie浏览器的各个版本.或者是他测试数据的时候ff某大版本的几个beta子版本还没出现?

其次是就你的加载器提到的预加载策略. 你有测过全部浏览器用object预加载可能涉及到的问题么(好比chrome,8,9的预加载的会话级别的资源类型缓存bug). 抛开这个问题不谈,假设你预加载到一半,用户再次触发了加载.你以为这种状况若是频繁发生.是否合适? 你的预加载策略连script.onload状态都没法测知,进一步优化的可能性就消失了. 考虑下为何seajs 的 umd要设计成那个样子?

最后吐槽下你的代码. 有注意到你用 document.body.appendChild 来像dom 中插入脚本. 个人建议是 永远不要这样作.除非你能够无视ie6用户.以及ie7缺失某些补丁的子版本.

你能够选择body 能够.但请用insertBefore. 但在某些极端状况下.这仍然会发生问题. 最佳实践是 head.insertBefore 向其第一个子节点插入.(你甚至无需检测是否存在子节点. 这个api会在没有子节点的时候,行为同appendChild). 而更加稳妥的状况是. 若是注入script. 发现document.head尚未被构建时. 能够本身造一个. 这才是一个通用加载器要作到的程度…

我也偷懒了,只是贴一下代码,顺便解释一下,界面把所依赖的js文件路径放在数组里,而后挨个建立script标签,src设置为路径,添加到head中,监听它们的完成事件。在这个完成时间里,咱们要作这么一些事情:在fileMap里记录当前js文件的路径,防止之后重复加载,检查列表中全部文件,看看是否所有加载完了,若是全加载好了,就执行回调。

1.4 小结

到此为止,咱们的简易框架的模块定义系统就完成了。完整的代码以下:

测试代码以下:

在这个例子里定义了四个模块,每一个模块只须要定义本身所直接依赖的模块,其余的能够没必要定义。也能够来这里看测试连接:http://xufei.github.io/thin/demo/demo.0.1.html