这几天稍微扫了一下CoffeeScript的部分源码,发现了一条挺有意思的算法,它解决了页面异步加载脚本时遇到的顺序问题。只是当初都没想过能够这样优雅地去处理这方面的问题。异步加载的脚本之间可能会有依赖关系,所以加载顺序就异常重要了。javascript
假如浏览器须要引入多个JavaScript资源,咱们通常会在页面上嵌入以下代码html
<body>
<script src="http://xxxxx.com/global.js"></script>
<script></script>
<script src="http://xxxxx.com/main.js"></script>
</body>
复制代码
默认状况下script
标签里面的资源会自动加载,而且这个过程是同步的,咱们并不须要担忧第一个script
标签请求完成以前浏览器就去执行第二个script
标签中的代码(超时或者是加上了aync这些属性的状况要另当别论了)。这种场景我姑且称之为脚本的同步加载。java
上述场景中script
标签至关于自动加上了type="text/javascript"
这样一个属性值,浏览器会自动识别这类资源并进行加载。可是若是加载的不是JavaScript资源呢?假设咱们要加载CoffeeScript资源,或许就会把代码写成这样git
<body>
<script type="text/coffeescript" src="http://xxxxx.com/global.coffee"></script>
<script type="text/coffeescript"></script>
</body>
复制代码
这种状况下,浏览器就不会自动加载script
标签里面的资源了,毕竟浏览器没法直接解析这种类型的脚本。github
若是要加载这类资源咱们则须要手动编写代码来遍历全部包含属性值type="text/coffeescript"
的script
标签,若是是带有src
属性则异步请求资源,若是没有src
属性则直接获取标签包裹的内容。经过特殊的脚原本执行加载好的CoffeeScript代码。ajax
这种场景中第一和第三个标签都须要经过发送请求来获取资源,这就会致使一种现象,若是不加特殊处理第二个标签里面的代码会比另外两个脚本先执行,而这个时候变量Global
尚未被定义,就会致使脚本出错。这种就是异步加载脚本的场景,异步虽好,它不会堵塞页面,不过要处理各个脚本之间的依赖关系也是个头疼的问题。算法
在不考虑使用打包工具的状况下我暂且提出这三个解决方案浏览器
回调无疑是最为简单粗暴的方式,以上的案例中只有3个JavaScript资源,构建一条完整的回调链彷佛没什么问题。不过仍是会使代码变得难懂,且恶心。回调很简单这里就不贴代码了。bash
若是我把script
标签增长到10个,而且其中包含几个内嵌脚本的话你应该不会再想用回调来解决了吧?案例以下异步
<body>
...
<script type="text/coffeescript" src="http://xxxxx.com/extern1.coffee"></script>
<script type="text/coffeescript"></script>
<script type="text/coffeescript" src="http://xxxxx.com/extern2.coffee"></script>
<script type="text/coffeescript" src="http://xxxxx.com/extern3.coffee"></script>
<script type="text/coffeescript"></script>
<script type="text/coffeescript" src="http://xxxxx.com/extern4.coffee"></script>
<script type="text/coffeescript" src="http://xxxxx.com/extern5.coffee"></script>
<script type="text/coffeescript"></script>
<script type="text/coffeescript" src="http://xxxxx.com/extern6.coffee"></script>
<script type="text/coffeescript"></script>
</body>
复制代码
固然,上面的只是示范代码,正常状况下咱们不可能这样去写代码。这种状况若是用回调去解决加载顺序问题的话,估计是我的都会崩溃了。咱们须要寻找更好的解决方案。
为了异步加载CoffeeScript资源,我先把伪代码写成这样
// 用于运行CoffeeScript代码
function handleCoffeeScript(cfCodeString) {
...
}
// 用于异步请求资源,返回Promise
function ajax(url) {
}
document.querySelectorAll('[type="text/coffeescript"]').forEach((item) => {
if (item.src) {
ajax(item.src).then((content) => {
handleCoffeeScript(content)
})
} else {
handleCoffeeScript(item.innerHTML)
}
})
复制代码
这代码咋一看彷佛没什么问题,尤为是这10个script
标签所涵盖的CoffeScript代码的业务逻辑彼此间没有任何依赖关系的时候,上诉代码彻底能够直接使用。然而,一旦它们之间有依赖关系,这样去加载脚本就会报错。我给10个脚本分别编号1-10,假设全部脚本都可以顺利加载,那么会出现下面的状况
2, 5, 8, 10 // 同步的内嵌脚本先加载运行
1, 3, 4, 6, 7, 9 // 须要异步请求的脚本后运行
复制代码
PS: 这只是一种状况,咱们永远没法保证先发送的请求会先响应,毕竟每一个接口的响应时间都不同,假设编号1中的资源比较大响应时间较长,那么执行顺序可能会变成3, 4, 1, 6, 7, 9。
一般为了解决这种问题咱们须要维护一个。初始化一个特定长度的队列,初始值值都是undefined。因为异步脚本都会在同步脚本以后才能被执行,为此能够在每次异步请求结束时都去检测队列是否已经满了,若是满了就证实全部脚本都已经加载完毕。接着依次执行队列中的每一项所包含的CoffeeScript资源。
// 用于运行CoffeeScript代码
function handleCoffeeScript(cfCodeString) {
...
}
// 用于异步请求资源,返回Promise
function ajax(url) {
...
}
const sources = document.querySelectorAll('[type="text/coffeescript"]')
// 初始化队列
let queue = new Array(sources.length)
// 检测队列是否已经满了
function checkQueueFull(queue) {
for(let i = 0; i < sources.length; i ++) {
if (queue[i] === undefined) return false
}
return true
}
sources.forEach((item, i) => {
if (item.src) {
ajax(item.src).then((content) => {
// 队列填充
queue[i] = content
// 队列若是塞满的话则依次运行全部脚本
if (checkQueueFull(queue)) {
queue.forEach(item => handleCoffeeScript(item))
}
})
} else {
// 队列填充
queue[i] = item.innerHTML
}
})
复制代码
这个脚本确实可以解决异步加载资源时遇到的顺序问题了,可是它显得有点笨拙,它必需要等到全部脚本加载完成后才可以依次去执行全部脚本。
假设编号为5的资源并非那么重要,并且加载时间会比较长,这种方式就会致使全部资源都须要等待编号5的资源加载完毕以后才有机会执行,这会致使脚本层面的堵塞。接下来咱们进一步优化这个流程,看如何规避这种问题。
为了优化这个过程,**除了上述的队列咱们还须要另外维护一个索引,每次异步请求完成以后检测当前索引所在位置的资源,若是这个资源已经加载好了,则执行当前位置的脚本,索引自增,再检测下一个索引所对应的资源是否可以执行,以此类推,直到遇到某个不可用的资源则中止执行。当再次发生异步请求的候重复上述过程,会根据索引值从以前中止的地方从新开启检测。**这一切能够以递归的方式实现,伪代码大概以下
// 用于运行CoffeeScript代码
function handleCoffeeScript(cfCodeString) {
...
}
// 用于异步请求资源,返回Promise
function ajax(url) {
}
const sources = document.querySelectorAll('[type="text/coffeescript"]')
// 建立一个等长的队列
let queue = new Array(sources.length)
// 脚本执行索引
let index = 0
// 执行函数,采用递归的方式,检测队列中当前索引的资源是否可用,若是可用则调用`handleCoffeeScript`方法来处理相关的内容,递增索引,并调用自身
function execute() {
param = queue[index]
if(param !== undefined) {
handleCoffeeScript(content)
index ++
execute()
}
}
sources.forEach((item, i) => {
if (item.src) {
ajax(item.src).then((content) => {
queue[i] = content
// 每次脚本加载完成都触发执行脚本,具体是否须要执行须要执行脚原本判断
execute()
})
} else {
queue[i] = item.innerHTML
}
})
复制代码
咱们来幻想一个比较极端的场景,假设1号和9号的异步请求都是慢请求,9号脚本的耗时比1号脚本长许多(假设是5s),那么加载程序运行起来会有如下表现
PS:简单起见,我暂时用脚本的状态来对队列中的每一项进行占位。
[undefined, "Available", undefined, undefined, "Available", undefined, undefined, "Available", undefined, "Available"]
复制代码
[undefined, "Available", "Available", "Available", "Available", "Available", "Available", "Available", undefined, "Available"]
复制代码
// 1号脚本加载完毕
["Available", "Available", "Available", "Available", "Available", "Available", "Available", "Available", undefined, "Available"]
复制代码
后续脚本会依次运行,但因为9号脚本加载时间太长,因此在对应的位置会中止执行,并等待
// 而后依次执行
["Executed", "Available", "Available", "Available", "Available", "Available", "Available", "Available", undefined, "Available"]
["Executed", "Executed", "Available", "Available", "Available", "Available", "Available", "Available", undefined, "Available"]
.....
["Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", undefined, "Available"]
复制代码
["Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Available", "Available"]
["Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Available"]
["Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed"]
复制代码
这个脚本的性能会比以前的脚本好上一些了,起码它不会等到全部资源都加载完毕以后才去执行。
一方面,2-10号的资源都须要依赖1号资源,它保证了1号资源加载并执行完毕以前不会执行任何其余的脚本。另外一方面,加载9号脚本须要比较长的时间,而咱们并不须要等到它加载完了才去运行其余脚本,而是会让在它以前的可以执行的脚本先行执行。只有10号脚本会等待9号脚本。
这篇文章简单地对异步加载脚本可能遇到的问题以及相关的解决方案作了个简单的阐述,虽然说真实环境可能不再会遇到这种问题了,不过了解一下算法仍是有好处的,说不定哪天遇到相似的场景就派上用场了。
本文只是用JavaScript写了些伪代码,算法流程也只是用文原本简单阐述,可能会致使有些地方表达不够到位。若是想更全面地了解这个加载脚本我建议直接看Coffeescript里面的源代码。