结构划分


操刀 requirejs,自己动手写一个_jquery

上面是我划分的项目结构:



  1. tool,​​工具模块​​,存放便捷方法,很多地方需要用到。
  2. async,异步处理模块,主要实现了 ​​promise​​​ 和 ​​deferred​​ 。逻辑上的异步。
  3. requirejs -> ​​loader​​ ,amd加载器,处理模块的依赖和异步加载。物理上的异步。


因为对于异步流程控制方面,研究过一段时间,所以这里第一时间想到的就是 promise ,如果用这个来做,所有的模块放入字典,路径做key,promise做value,所有依赖都结束之后,才进行下一步操作。 不用管复杂的依赖关系,把逻辑尽量简单化:



  1. 首先有一个字典,存放所有的模块。key放地址,value放promise,promise在模块加载完毕的时候resolve。
  2. 如果依赖某个模块,先根据路径从字典找key,存在就用该promise,不存在就去加载该模块并放入字典,并使用该模块的promise。
  3. 所有的模块,我只用它的 promise ,在它的回调中写我的后续操作。它的resolve应该单独抽离出来,属于异步加载方面。


大致思路有了,当然实际写的时候肯定困难重重,不过没关系,遇到问题再去解决。

考虑到代码的简易性,以及我的个人习惯。我打算用类似于 ​​jquery 的 $.Deferred() 和它的promise​​​,与es6的promise有一定的出入。这样代码书写更简易,并且逻辑上更清晰,es6的promise用起来确实稍显麻烦。我需要的是一个 ​​pub/sub​​ 模式,一个地方触发,多个回调执行的并行方式,es6的promise,需要在then中一次次返回,并且resolve起来也不方便,最最主要的是需要 polyfill 一下,而我想自己写,写我熟悉且喜欢的代码 。


callbacks模块


回调模块 ​​callbacks​​,熟悉jquery的朋友接下来可能会觉得使用方式很熟悉,没错,我受jq的影响算是比较深的。以前在学习jq源码的时候,就觉得这个很好用,你可以从我的代码里面看到jq的影子 :

操刀 requirejs,自己动手写一个_异步加载_02 View Code


这是一个工厂方法,每次所需的对象由该方法生成,用闭包来隐藏局部变量,私有方法。而最后暴露(发布)出来的对象,用 pub/sub 模式,提供了 ​​订阅​​​ , ​​触发​​​ ,​​禁用​​​,​​查看禁用​​​ 4个方法。 这里要说的是 ,提供了3个参数:​​stopOnFalse​​​、​​once​​​、​​memory​​​。触发的时候,按照订阅顺序依次触发,如果是 ​​stopOnFalse​​​ 模式,当某个订阅的函数,返回是 false 的时候,停止整个触发过程。 如果是 ​​once​​​ ,表示每个函数只能执行一次,在执行过后,会被移除队列。而 ​​memory​​ 状态下,在 callback 触发后,会被保持状态,之后添加的方法,添加后会直接执行。

这三种模式,传参的时候直接传入字符串,可以随意组合,用空格分开,比如:​​callbacks('once memory')​

该模块用于整个项目中,处理所有的回调。使用方式类似于jquery的:​​$.Callbacks(...)​



deferred 模块

deferred ,是对promise的父级模块,主要提供了 触发 和 订阅 2个方法。 promise 是对 deferred 的一个再封装,仅仅暴露出其中的 订阅 方法。

从概念上来说,很像 C# 中的委托和事件。

操刀 requirejs,自己动手写一个_异步加载_02 View Code

​deferred​​​ 使用了 ​​callbacks​​​ 模块来处理其中所有的回调函数。是一个工厂方法,​​deferred()​​​ 返回的是一个deferred对象(发布),包含了3种状态:​​pending​​​,​​resolved​​​,​​rejected​​​;提供了 ​​then​​​ 和 ​​catch​​​ 去订阅;通过 ​​resolve​​​ 和 ​​reject​​ 去 改变(触发) 状态。

deferred 对象,提供了一个 promise() 方法去返回一个promise对象,区别就是promise对象屏蔽了触发的方法。就像委托和事件,前者可以订阅和触发,而后者只能订阅。之所以如此,是想只提供订阅的接口,而如何触发,何时触发,由我自己控制,是我逻辑内部的事情,而其他部分,只需要知道也只能去订阅。

Tuple ,是一种约定的、按照某个规则进行存储的数据结构(类?), c# ,typescript 中都有这个东西,之前在学习jq的时候,看到了它的内部也这么用,于是学到了。其实在我看来,使用tuple,就是节约代码,笑。不必要去定义某个类,或者其他的东西,只需要在定义和使用的时候,遵循某个约定好的规则,那么就可以省去一大堆的代码,让逻辑部分也清晰不少。


all 模块

操刀 requirejs,自己动手写一个_异步加载_02 View Code


all,其实就是es6中, ​​Promise.all​​​ 或者 ​​$.when​​​ 的一种实现。参数是一系列的promise,本身返回一个promise对象,在所有参数中的promise对象都处于 ​​resolved状态​​ 时,本身也会被resolve掉,由此来执行通过then订阅的方法。

all本身,是通过一个触发器来实现在最后一个promise完成时回调。内部用一个int值来存储resolved的参数的个数,给每个参数通过 then 添加一个回调来执行这个触发器,当 ​​完成数量 >= 参数个数​​ 的时候,就表示所有promise已经完成,可以进行后续的操作。 用 >= 来代替 == 是个好习惯 :D




模块分析 模块定义、模块获取

到此为止,async 部分已经完成,准备工作已经做好。我们开始 amd 模块部分的分析。

amd 模块在我看来,主要分为两个部分:​​模块定义​​​、​​模块获取​​。先说模块获取:

模块获取

操刀 requirejs,自己动手写一个_github_05

模块的获取,并不复杂。先从字典中根据路径(key)去找该模块,如果有该模块,就去加载。如果不存在,就去加载该js,根据onload来确定该模块的名称(如果是匿名模块);然后根据该模块的返回值==》 一个promise,给该promise添加一个回调,去管理 getModule 的返回值状态==》另一个promise。在使用一个模块的时候,从本质上来讲,是给该模块的promise的then接口添加回调函数,一层层往下处理。

模块定义

这里的重点是 加载模块,大家都知道,amd的每个模块,对应一个js文件,加载模块就是去加载这个js。

再看看模块的定义,有 3种重载:


  1. define(sender)
  2. define(deps,sender)
  3. define(name,deps,sender)

sender 是一个函数,或者某个对象。deps 是一个数组,表示该模块依赖的其他模块。name 是表示当前模块是一个命名模块,强制使用该名称,一般是打包工具生成这种模块,不建议自己直接这么写。

从上面我们可以看到,模块是通过执行一个函数,用传参的方式把所要用到的模块加载到某个地方保存起来。那么看到这个你们有没有想到什么呢?我首先想到的就是 jsonp ,动态执行一个函数,把数据放进去,对得上,完美。从这个思路,我实验了一下,在这里直接说结论: script标签在动态加载到页面后,首先去服务器拿对应地址的数据,然后在文件下载完全后,执行该js文件中的内容,执行完毕后,会触发该script标签的load事件。

也就是说,通过给load事件注册方法,我们可以知道最后一个加载的模块(js文件),来自哪里,什么时候执行完全。这样就确定了,并行加载多个js文件时,匿名模块所属来源。这里不讨论兼容的问题,低版本ie对应的是其他事件:onreadystatechange,我没用过。


操刀 requirejs,自己动手写一个_异步加载_06

在模块加载后,我们用一个函数来将模块填充到字典中,类似于一个 ​​触发器​​,每次加载一个模块,模块中包含这个函数并执行,处理依赖关系,并将最后的结果保存。

在模块的加载中,因为可能会同时加载多个模块(js文件),并不能确定到底是哪一个先加载完全。但是我们知道,js是单线程,在js文件下载完全后,会先把js文件中的内容执行完毕,然后再触发load事件,这个顺序是可以保证的,所以就可以使用一个变量来保存最近加载的模块,来知道匿名模块的所属路径。

不论是匿名模块,还是命名模块,都可能依赖其他的模块,所以并不能确定在模块加载完之后,就可以立即使用,要等待所有的依赖项都加载完毕,所以一个模块的最终返回值我使用的一个promise来保存。这样就可以方便的在状态变更后才添加下一步的处理操作,从逻辑上简化整个流程控制。

模块入口 require

操刀 requirejs,自己动手写一个_异步加载_02 View Code

这里的代码比较简单,唯一要注意的就是这个 ​​setTimeout(action,0)​​ 。因为js是单线程,从上往下依次执行。模块可能会被打包工具合并成一个文件,那么在一个文件中就含有了模块入口、命名模块。如果模块入口在最上方,,,在依赖某个命名模块的时候,就会试图去加载这个名称的js文件,而这注定是会失败的。所以使用一个setTimeout,把模块入口的逻辑,放入事件队列中,让js逻辑线程优先去执行文件后面的代码,就避免了这个问题。

loader 模块代码

操刀 requirejs,自己动手写一个_异步加载_02 View Code


core 模块

操刀 requirejs,自己动手写一个_异步加载_02 View Code

core,主要存的是一些配置信息,和模块的字典,比较简单。