背景
在不反对打包构建的前端历史我的项目中,咱们常常会有动静引入 script 的诉求,为了书写不便,开发的时候会习惯性的用 jQuery 生成 script 标签再 append 到 body 中,并将其视为和原生写法性能完全一致
// jQuery
var $script = $('<script src="xxx.js">');
$('body').append($script);
// ECMAScript
var script = document.createElement('script');
script.src = 'xxx.js';
document.body.appendChild(script);
但它们真的统一么?jQuery 在这里什么都没做么?不同写法对咱们又会有怎么的影响呢?
先说论断
jQuery 写法与原生相比差别如下:
- 并非通过规范的 jsonp 形式加载 JS 内容,而是通过一个 xhr 申请
- 此 xhr 是同步的,而动静 script 则是强制 async 的
- 非法的 type 赋值会让 xhr 申请不发送(不写 type 是非法的,要写就写对
除了那个同步异步的操作,是不是看起来对咱们理论开发影响不大?如果真不大我也不会有情绪探索这个问题了,这里须要额定补充一个 xhr 和 jsonp 申请 JS 内容的差别点:
- 重点 :在 iOS 环境下,浏览器 cache 的 JS 申请并不会被用于 xhr,缓存的形式包含刚申请过、提前 prefetch 等
所以如果对同步插入没有诉求,能不必 jQuery 插入就不要用
理论场景
下面我提到了,iOS 下的 xhr 申请 JS 是不会走缓存的,那么这个影响到底大不大呢?
咱们的我的项目因为一些非凡起因,会在页面内容显示前动静插入 7 - 9 个 script 标签,且均是通过 jQuery 插入的,大小 1KB – 200KB 不等,不算小但也没有特地夸大,在 PC 模仿环境下加载和执行耗时如下:
如果容许 xhr 应用缓存的话,对应的耗时如下:
但这部分 JS 在挪动端用户的理论应用场景下,加载和执行的理论统计工夫如下:
样本里安卓和 iOS 用户数量基本一致,也没有太多极值,能够了解为比安卓多的局部就是本起因导致的。这可不是 95 线、75 线那种较差环境下的指标,光失常的均值和中位数都无能到 1s 以上,要晓得页面关上满共才 2 -3s,如果碰到网略微差一点,那这个页面就奔着 5s+ 去了
解决计划
现实的状况当然是把这些 JS 原地位替换成原生写法即可,但在咱们我的项目里有两个艰难点:
- 这些动静 script 是有同步要求的,必须在引入的中央执行,不然会缺失必要的上下文
- 引入的反复书写十分多(300+),而且因为历史起因,代码还存储在数据库而非代码库中,无奈做到平安的全量批改
最初我的计划是: - 批改 jQuery 源码,拦挡这些已确定的 script,不让其走 xhr 申请
- 在原引入逻辑的上方通过 script 标签失常引入确认拦挡的 JS
- 将拦挡的 script 的非 Function 申明局部都装在各自的 init 办法中,在原来引入的中央执行 init。就相似于引入 Vuex 后须要在适合的机会执行 Vue.ues 原理一样,基于 Vue 的库都会有启动函数,我也将咱们动静的几个 JS 的 立刻执行局部 也别离装进了各自的启动函数,并在原地位执行,模仿同步执行的过程,以便缩小逻辑影响
最初的成果就是常见的动静 script 都会通过 jsonp 规范模式而非 xhr 的形式来申请下发,并顺利借道浏览器缓存,预期 iOS 加载时常能够升高到 Android 的程度
jQuery 源码剖析
下面我说针对动静 script,jQuery 会走 xhr 来进行申请,但仔细的小伙伴在 HTML 里一搜,发现明明有对应 <script> 标签呀,但网络上也发了 xhr 的申请,这是怎么回事呢?这些就须要看看源码了
// 咱们应用 jQuery 插入动静 script
// 留神通过 jQuery 生成的这个 script 和原生是存在轻微差别的
// 例如用原生形式插入本 script 标签也不会发 jsonp 申请,起因我还没找到
// 所以要走规范 jsonp,就都须要用原生书写,不要偷懒
var $script = $('<script src="xxx.js">');
$('body').append($script);
// jQuery 源码(截取并减少正文)// append 办法
append: function() {
// 发 xhr 的逻辑在这个 domManip 中,咱们等下看
return domManip(this, arguments, function( elem) {
// 这里是回调函数,会在针对 script 标签进行加工后、发 xhr 前执行
if (this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9) {var target = manipulationTarget( this, elem);
// 原生的插入动作,也正是有这句,才让咱们能够在 HTML 中搜到 script 标签构造
// 但为什么标签明明在,却没发 jsonp 申请呢?往下看
target.appendChild(elem);
}
});
},
// domManip 重要局部截取
function domManip (...) {
...
// 所有 script 标签都被 disableScript 办法加工了
scripts = jQuery.map(getAll( fragment, "script"), disableScript );
...
// 发送对应 xhr 申请
jQuery._evalUrl(node.src);
...
}
// 下面两个片段咱们离开说
// 先说 disableScript 做了什么
function disableScript(elem) {
// 通过这行后,script 标签的 type 会被加工,模式例如【trur/text/javascript】// 很显著,后面加上 boolean 的 type 无论如何都是不非法的
// 所以这个奇怪 type 的 script 标签会被当作文本块,而不是发网络申请
// 这个 type 会在前面的逻辑中修改回来,但已插入的 script 标签批改 type 并不会让它从新申请
elem.type = (elem.getAttribute( "type") !== null ) + "/" + elem.type;
return elem;
}
// 再看看_evalUrl
jQuery._evalUrl = function(url) {
// 须要留神,因为是走 xhr,所以全局的拦截器设置是失效的,就可能会对本次申请进行加工,例如加上工夫戳之类
return jQuery.ajax( {
url: url,
type: "GET",
dataType: "script",
async: false, // 留神这里,是同步的,动静 script 标签也只有通过这种形式能力做到同步
global: false,
"throws": true
} );
};