问题的起源是这样的:
假设加载有2个脚本A和B,而a和b则是附加在各自脚本上的onload侦听。众所周知的,IE下A加载完成exec后a并不是保证立刻触发的,非IE下正常。但是,倘若前提是A的加载完成和B的加载完成顺序确定,即A->B,那么a->b的顺序能不能得到保证?即:有可能是AaBb、ABab,但绝不可能有ABba。
自己测是发现不了问题,但显然范围太小不足以说明情况,所以我借助土豆的某个次要页面流量,做了个大范围精度测试。
目标:测出IE6、7、8、9下面的顺序情况。
理由:懂的入。
用例:用户有25%的几率加载a.js、25%的几率加载b.js、剩余50%全部加载,以此模拟出各种可能被缓存的情况——即可能先缓存a,然后再加载a和b。
数据结构:第一位的0和1标明页面是否发生error,是的话1,否的话0;后面大写字母是script标签被加载exec时记录,小写字母是其onload触发时记录;再后面数字是次数。
结果:近千万份数据,可能因对浏览器ua的判断稍稍有些误差。
error
ie6
“1Aab” “3410″
“1a” “2979″
“1b” “2901″
“1Bba” “1575″
“1Bab” “136″
“1Aba” “61″
“1″ “56″
“1Aa” “12″
“1A” “10″
“1ab” “9″
“1Bb” “7″
“1ABab” “4″
“1AB” “1″
ie7
“1b” “771″
“1a” “743″
“1Aab” “738″
“1Bba” “376″
“1″ “108″
“1Bab” “23″
“1Aba” “15″
“1Bb” “10″
“1Aa” “3″
“1ABab” “1″
ie8
“1Aab” “2460″
“1a” “2050″
“1b” “1887″
“1Bba” “1290″
“1″ “322″
“1Bab” “95″
“1Aba” “39″
“1Aa” “20″
“1Bb” “11″
“1ABab” “1″
ie9
“1Aab” “372″
“1a” “321″
“1b” “285″
“1Bba” “218″
“1Bab” “10″
“1Aba” “6″
“1″ “1″
“1Bb” “1″
success
ie6
“0ABab” “1591785″
“0Bb” “1093518″
“0Aa” “1093213″
“0AaBb” “373421″
“0BbAa” “137775″
“0BAba” “79495″
“0ABba” “4″
“0ab” “1″
“0aBb” “1″
“0BaAb” “1″
ie7
“0ABab” “371165″
“0Aa” “257674″
“0Bb” “257001″
“0AaBb” “88582″
“0BbAa” “33734″
“0BAba” “20710″
“0ABba” “108″
“0BAab” “23″
“0BaAb” “2″
ie8
“0ABab” “1108442″
“0Aa” “794383″
“0Bb” “792879″
“0AaBb” “313784″
“0BbAa” “108791″
“0BAba” “54147″
“0ABba” “4″
“0BaAb” “1″
ie9
“0ABab” “188309″
“0Aa” “132499″
“0Bb” “131587″
“0AaBb” “44003″
“0BbAa” “19110″
“0BAba” “12620″
“0ABba” “211″
“0BAab” “40″


总错误数 23338
总成功数 9099023
错误率在0.256%
总错误数 23338
总成功数 9099023
错误率:2.56‰
error中除去加载顺序正确的错误数
总错误数 16185
总成功数 9099023
错误率:1.776‰
还是有些不能接受啊!
我怀疑这些error是国产加壳浏览器所为。
这个结论应该正确,我也做了个测试:
http://seajs.com/test/issues/onload/test.html
在各个浏览器下都测试没问题。看来可以通过这个方法获取到匿名模块对应的id,明天来看能否基于这个规律,优化下seajs在ie下的url获取逻辑。
之前微博上讨论那个,和承玉回忆了一下,是当有缓存时,getInteractiveScript 这种方式会失效,要采用“同步”获取的模式来获取url,最近发现cached时,同步模式获取url在ie6下有时也会失效,希望army这个研究能彻底解决ie6的url获取问题,明天尝试下,感谢。
从测试页面:http://js.tudouui.com/js/events/onload/index.html
上看,是整个完整的 loader
建议简化化,和我那个测试页面一样,尽可能简单,这样更准确。
此外,不建议用 Image.src 来做缓存,这个有问题,不靠谱。
早上起来有个疑惑点,把测试页面的代码又看了一遍
昨晚的回复有个错误,没有用 Image.src 做缓存,我看错了
测了几个主流的国产壳,一切正常
对那些错误,困惑了
@lifesinger: 因为是作为iframe嵌入某个页面的,可能在加载到一半时用户关闭了或点击链接跳转了。还有可能是网络问题导致某个js加载404。然而报错的信息为何会是那样还是很奇怪。
阅读了下 view-source:http://js.tudouui.com/js/events/onload/index.html, 几点交流:
1. exports 和 module 也是全局的?
2. 一个改进点:load 函数里,如果是 loading 状态,可以再注册一个onload就好,不需要自己维护 callback 列表。参考 seajs/src/fn-load.js#L150
3. 去除注释和提取依赖的正则,可以增强下:https://github.com/seajs/seajs/blob/master/src/fn-define.js#L90
4. fac 用 factory.toString() 做键值,虽然有 callee.caller 来保驾,但感觉这个键值好大,太长时不知会不会出问题,还有个隐患时,如果两个模块的 factory.toString 一样,后 define 的会覆盖先 define 的。这个设计感觉怪怪的。
其他的逻辑我没细看了,看起来是 AMD 的逻辑,和 seajs 的不同。
seajs 我在基于 ABab 这种逻辑重构下。
@lifesinger:
1.我对规范的理解有误吧,把它当作全局的了。
2.我看看。
3.其实之前是去除注释的,后来不知为何又被我给删了。
4.不会覆盖的。那是个hash链的结构设计,相同键值是同一个hash,但被存在了一个数组中避免冲突。这是我想出来的依赖模块的相对路径设计。
@army: 在一个页面中的模块数量是可预见的,所以我觉得那个hash所占内存不会有什么性能瓶颈的问题。
@army: 哦我想起来,因为构建工具过程中工具已经去除了注释,所以不需要考虑注释。
如果键太长的话,可以考虑压缩一下,反正有hash链冲突的解决方案。(java的hashmap冲突解决算法)
下午开始重构,删了100多行代码,后来测试才发现根据回调顺序,信息依旧不完备,悲催了,回滚ing
假设 a.js 文件里,定义了 define(‘具名模块’)
b.js 里,定义了匿名模块 define(factory)
具名模块可以在 define 运行时会确定好所有信息,直接 memoize
匿名模块延迟到 onload 阶段
当一个文件一个模块被严格遵守时,ABab 很有用,信息很完备。
但是是 a.js 里,有多个模块时,比如打包后的文件时,就无法确定 defQueque 的边界,确定不了哪些模块是 a.js 定义的
理想情况下,a.js 和 b.js 应该各自往 defQueque 添加一个 mod
真实情况下,a.js 往 defQueque 添加的项可能是 >= 0 的任意值
只要不是 1,defQueque 中的信息就不完备。还是得用以前的方式来做。
郁闷ing
@lifesinger: 这个我知道,所以我在打包的过程中做了一些“小动作”,被打包进来的模块是不进入defQueue的。
嗯,在构建时增加约定是没问题的,能保证一对一应该就没问题。
seajs 的用户习惯比较原始,还是继续保持老方法来获取url。这样,一个模块里不用限定模块数,写了id的情况下,能和YUI3的方式保持兼容,也能使用combo服务,不用事先合并。
@lifesinger: 其实我觉得1对1是个更好的拆分约定。1对1的强制约定就像java的一个文件只允许1个public class一样。在构建过程中强制1个文件1个模块,对开发维护阅读都是好处。合并或压缩之类的,应该交由工具去做,甚至这个工具对开发者来说都是不可见的。我最初设计的考虑就是只需开发单个的模块,后续工作开发者根本无需关心。
你因为兼容考虑不像我这样不用担心什么哈哈,其实这个东西目前还是个测试状态,离实行还很远。(历史遗留问题 T T)。
不过我感觉你可以考虑在seajs2中进行不兼容的升级。
@lifesinger: 我突然想起来似乎还是有办法解决你的问题的。0的话好办,说明没有,可以在onload判断下。>1的时候,其实还是借助打包工具,做小动作替换下这些文件?概念有点模糊,一时想不起来。
另外我还有些疑惑。比如一个被合并起来的多个define文件,你的seajs里是否要求仅允许出现一个匿名模块?这些多个define的模块的uri是否相同?如果可以合并另外一个文件的匿名模块,那这个模块的id和uri又是什么?
seajs中一个文件中只能允许出现个一匿名模块,出现多个的话前者会被后者替换。
对于未打包的文件来说,一对一为佳,这样才能真正体现匿名define的意义,在看seajs时我也一直奇怪为何会出现这种有歧义的规则。
再者在从维护以及可读可查找的角度来说一对一也是更好的。
@army: seajs 1.0.x 差不多功能齐备了。现在其实在搞 1.1.0, 可以考虑部分不兼容,呵呵。
对于你的代码中, define 参数的 combo 和 url,建议放到另一个方法,比如 attachMod, 专门用作打包后的模块,且这个方法不放在 public api 里。
API 的设计里,有一个原则:如果是私有的,就一定搞成私有的。以前不是很理解,现在越发感觉到这个原则很重要,一旦外部可设置,经常就真的有人会去用,结果为了兼容,原本意图私有的方法会逐步变成 public
比如 SeaJS 里,最初的意图就是一个文件一个模块,且必须是匿名模块,格式必须是:
define(function(require, exports, module) { … })
但为了实现上的方便,我把 id 和 deps 也放在 define 参数上了,结果就有人发现了这一点,很兴奋的去用,像用 YUI3 一样用的很 happy,实际上是和我的初衷违背的,匿名模块是比具名模块更 DRY 的一种写法。但现实情况下,大家习惯了 YUI3 这种具名模块的写法,因此反而一时半会不习惯匿名模块,初衷就在这种现实情况下发生着变化。
后来还有 combo 的问题,spm 项目进度比较慢,结果内部出现不少自主打包工作⋯⋯
不一一提了。
匿名模块肯定只能出现一个,多个的话,后者会覆盖前者。
SeaJS 的打包,之前采取了虚拟模块的概念,比如将 x/a.js 和 x/z/b.js 打包在一起,会是:
define(‘./a’, …);
define(‘./z/b’, …);
打包后的文件假设是 http://t.com/path/to/combo.js
则解析 combo.js 时,等价于定义了两个虚拟模块:
define(‘http://t.com/path/to/a.js’, …);
define(‘http://t.com/path/to/z/b.js’, …);
因此不光是匿名模块的问题,最大的问题是对这种相对路径的虚拟模块的兼容,这些虚拟模块肯定是多个存在于同一个文件中的。
也有解决办法,更改 spm 的打包策略,不用相对路径,打包时直接达成绝对路径,这样对性能也有好处,但缺点时,打包时,用户必须指定上线后,js 文件存放的基础路径。
@lifesinger: 对于combo和url我也纠结了很久,一直没想出更好的方案。
我也更喜欢纯匿名的解决方式。
@lifesinger: 你的打包策略和我考虑的不谋而合哈哈。
@lifesinger: spm最后那句,我记得有设置location.host的api在,貌似是base吧,可以考虑相对根路径。
关于那些error,我有了些新的观点。ie的onload有极低的几率可能在它本身前触发!
因此发生了像AabB这个样子。但是b执行的时候没有B,于是报错Aab……
能想到的解决方案是抑制网络风暴的2次幂延迟算法。
@lifesinger: 其实我觉得1对1是个更好的拆分约定。1对1的强制约定就像java的一个文件只允许1个public class一样。在构建过程中强制1个文件1个模块,对开发维护阅读都是好处。合并或压缩之类的,应该交由工具去做,甚至这个工具对开发者来说都是不可见的。我最初设计的考虑就是只需开发单个的模块,后续工作开发者根本无需关心。
你因为兼容考虑不像我这样不用担心什么哈哈,其实这个东西目前还是个测试状态,离实行还很远。(历史遗留问题 T T)。
不过我感觉你可以考虑在seajs2中进行不兼容的升级。
+1