这一篇可能和TypeScript没太大关系。
在这篇之前,可以先看看《Evaluate之迷思》。
看到TypeScript实现的script接口(去掉了deprecate的属性),除了我们平时经常用的几个属性之外,还可以添加监听,倒是让人耳目一新。
interface HTMLScriptElement extends HTMLElement {
async: boolean;
defer: boolean;
src: string;
text: string;
type: string;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLScriptElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLScriptElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
这个本身没什么疑问,但是之前看到一个问题,着实让我吃惊不小:
<head>
<script src="scripts/test1.js" async></script>
<script src="scripts/test2.js" defer></script>
<script>
console.log('normal');
</script>
</head>
// output: normal test2 test1
这个输出顺序有点不太对劲。因为我们知道,<script>标签是阻塞的,浏览器读到就会立刻进行编译执行那一套,后面的内容全得等着。这个在之前的文章里提到过。而async是异步的,相当于是另外的一个线程,浏览器在解析DOM的时候它在下载,下载完了立刻编译执行,当然这个执行的过程是阻塞的,因为虽然是所谓的线程化渲染,但JavaScript还是只能在内核里执行;浏览器就一个内核。这个在前面也提过。至于defer,则是等页面渲染完了再执行,按照规范上的要求,是要在DOMContentLoaded
事件触发之前完成。这个时候其实阻不阻塞的影响已经没那么明显了,因为页面已经渲染完成,至少不会出现白屏。
这些都是没什么问题的。但是问题在于,这个输出顺序,似乎没按照规范的要求来。normal出现在两个test之前还情有可原,因为JavaScript是单线程的,test1脚本下载编译那一套搞完了准备执行的时候,normal已经在执行了,test1就只能等着;我觉得这里的实现可能和事件队列差不多。但是至少test1应该在test2之前啊?从直观上来说,defer应该在最后执行。为了确认自己的理解没错,我专门去看了HTML Standard。按照上面的说法:
For classic scripts, if the
async
attribute is present, then the classic script will be fetched in parallel to parsing and evaluated as soon as it is available (potentially before parsing completes). If theasync
attribute is not present but thedefer
attribute is present, then the classic script will be fetched in parallel and evaluated when the page has finished parsing. If neither attribute is present, then the script is fetched and evaluated immediately, blocking parsing until these are both complete.
这个图也来自HTML Standard:
理解是没错的。但是为什么呢?后来我刷新了几次,惊奇地发现,输出顺序又变了,变成了normal test1 test2。这次defer变成最后执行的了。用console.timer
分析一下时间,发现test1和test2的执行时间是会经常变化的,并没有一个固定的先后顺序。
规范是规范,实现是实现。在应用中,为了性能,浏览器或多或少会有自己的“优化”。事实上,defer和async的evaluate顺序是不一定的,用Chrome的开发者工具的performance看一下就知道。这一切都是不确定的,因为都是异步实现。就算和浏览器没有关系,异步的顺序也没有一个绝对的保证。还记得《JavaScript高级程序设计》里是怎么说的吗?
在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在DOMContentLoaded事件触发前执行,因此最好 只包含一个延迟脚本。
所以,面对异步问题的时候,我们绝对不能依赖于直观感受上的顺序,应当慎重。看到一位dalao总结得很好:
- defer 和 async 在网络读取(下载)这块儿是一样的,都是异步的(相较于 HTML 解析)
- 它俩的差别在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的
- 关于 defer,此图未尽之处在于它是按照加载顺序执行脚本的,这一点要善加利用
- async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行
- 仔细想想,async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的,最典型的例子:Google Analytics
参考资料
目录
从TypeScript视角看HTML DOM(二):Node与Element
从TypeScript视角看HTML DOM(三):NodeList与HTMLCollection