细说dom就绪事件
OK,现在来谈谈DOM就绪事件。 这个问题,关系到浏览器内核实现,但我们作为前端,能力不及。便从表象上去理解它,进而推断出对前端开发有所帮助的实践方案,就够了。
OK,浏览器DOM加载顺序是什么样的呢?
可以参考IBM的一篇文章:http://www.ibm.com/developerworks/cn/web/1308_caiys_jsload/
据我观察,现代浏览器首先会加载html扫描一遍要下载的资源,开启并行下载,而且这个并行下载并不阻塞浏览器开始执行html。
接下来,浏览器就从头开始执行html,问题来了: 如果执行到js,就阻塞浏览器,阻塞html往下执行,后面的html执行都要等待他下载完并执行完。 而且这个js执行的时候,也会阻塞浏览器那些并行的下载动作。
接下来,html继续往下执行,如果有css会加载并解析css,然后构建DOM,并渲染页面。
直到执行到文件结尾body之前,此时dom结构创建完毕,readystate可能到达了comlete或者lodaed阶段,或者说DOMContentLoaded触发。 此时你便可以干活了,比如绑定dom事件等。 但是页面里面的图片等资源还没加载完,等他们加载完了,就触发window.onload.
那么,如何解决js在头部加载和执行时,对页面造成的阻塞呢?
这个脚本阻塞带来的一个问题就是,用户提前看到白屏。
所以,采用如下方法解决:
(1)让阻塞发生的晚一点。 怎么办呢。 很明显,把js加载和执行位置挪一挪呗,把它放到页面body结束标签之前。这样阻塞发生的时候,其实页面的所有可见内容已经渲染完了,用户至少看到html和css啦。
(2)让阻塞发生的晚一点,但是操作优雅一点。 当然,让阻塞发生的晚一点,还可以用比较潮流的async和defer加载。 async属性表明这个文件需要异步加载,不会阻塞页面渲染dom操作,避免网页失去响应,但他一旦加载完,很可能就执行了,所以你要思考这个js文件的依赖啥的(万一他去操作dom但dom还没构建完呢,当然dom构建是挺快的)。
IE不支持async这个属性,只支持defer,所以把defer也写上。 defer是延迟执行,等dom加载完了,再执行这段js。 当然这个js的下载是会异步跟其他资源并行下载的。
(3)比较高大上的解决方式:动态装载脚本,哈哈。 这是目前很多前端模块加载器使用的方法。
使用动态创建script元素来加载,这种方式是用js脚本构建一个script元素,再把它apppend到页面的head里面,这种方式添加的脚本“下载和执行”都不阻断页面渲染,哪怕是添加到head标签在head里面执行,也不会影响页面渲染,所以很适合用。 然后,通过script元素的onload或者onreadyState事件来监听这个脚本有没有加载执行就绪。
其实,这里有个比较严重的问题,就是这种“鸡蛋”总该有个“鸡”来生出来呀。 总该有脚本来装载脚本啊。 所以装载脚本的执行还是会阻塞页面的呀。 故: 前置的装载脚本, 还是得用前面讲到的延迟执行方法。
(装载进来的js,浏览器默认就把它执行了。怎么让这种方式加载来的js代码在我想要执行的时候再执行呢,答:学一下AMD等模块加载器的实现,弄一个loader,然后加载js依赖的时候其实加载一个由define函数定义的module,他会把js代码注册到loader里,在依赖他的时候再执行。)
(4)使用xhr对象来下载js脚本内容,然后xhr就绪后,可以在想要运行脚本的时候,往页面中append一个script标签,把脚本文本设置进去。但必须同域。
(5)真的使用模块加载器! 使用模块化工具异步加载且保持依赖关系。AMD是提前加载和执行,CMD是用时执行。AMD模块写成commonjs风格的话,其本质上是loader把工厂函数给toString,然后正则匹配出里面的require依赖,给提前加载和执行了。
所以AMD永远是把依赖先加载并执行完了,再执行factory里面的代码。
AMD好处:提前执行若有错误及时报,不会执行引用模块的代码。 CMD的话可能执行到依赖项的时候,才发现依赖模块执行出错了。 require提前执行的方式就像工厂提前把原材料准备好再干活,干起活来自然不会有卡顿。