JavaScript异步加载防错工具:Sniffer.js原理与实现
本文介绍Sniffer.js工具,旨在解决JavaScript异步加载时的方法调用报错问题。通过检测命名空间存在性并缓存未就绪的方法,该工具确保在依赖文件加载完成后自动执行,避免运行时错误,提升前端开发的稳定性与可靠性。
实战代码示例
一、热身——先看实战代码
a.js 文件
// 定义Wall及内部方法
;(function(window, FUNC, undefined){
var name = 'wall';
Wall.say = function(name){
console.log('I\'m '+ name +' !');
};
Wall.message = {
getName : function(){
return name;
},
setName : function(firstName, secondName){
name = firstName+'-'+secondName;
}
};
})(window, window.Wall || (window.Wall = {}));
index.jsp文件
<script type='text/javascript'>
<%
// Java 代码直出 js
out.print("Sniffer.run({'base':window,'name':'Wall.say','subscribe':true}, 'wall');\n");
%>
// Lab.js是一个文件加载工具
// 依赖的a.js加载完毕后,则可执行缓存的js方法
$LAB.script("a.js").wait(function(){
// 触发已订阅的方法
Sniffer.trigger({
'base':window,
'name':'Wall.say'
});
});
</script>
通过这种机制,无论a.js文件体积大小如何,Wall.say('wall')的调用都会等待文件完全加载完毕后执行。
工具简介与背景
二、工具简介
// 执行 Wall.message.setName('wang', 'wall');
Sniffer.run({
'base':Wall,
'name':'message.setName',
'subscribe':true
}, 'wang', 'wall');
观察上述执行代码,可能会感到疑惑——这究竟是如何实现的?😆
Sniffer.js的核心作用在于试探性地执行方法。若方法尚不可用,它不会抛出错误。
例如调用Wall.message.setName('wang', 'wall');
即使该方法所在的文件尚未加载完成,也不会引发报错。
其处理逻辑是先将调用请求缓存起来,待方法加载完成后,再执行调用。
再次调用的方法如下:
// 触发已订阅的方法
Sniffer.trigger({
'base':Wall,
'name':'message.setName'
});
在线demo:https://wall-wxk.github.io/blogDemo/2017/02/13/sniffer.html (需要在控制台看,建议用pc)
该工具的诞生源于公司业务需求,由作者自行编写。
由于公司后台语言为Java,习惯使用jsp的out.print()方法直接向客户端输出JS方法并执行。
这导致一个矛盾点:有时JS文件尚未下载完毕,后台输出的语句已开始调用方法,造成尴尬局面。
因此,该工具具备两个主要功能:
1. 检测执行的JS方法是否存在,若存在则立即执行。
2. 缓存暂时不可用的JS方法,待其真正可执行时,从缓存队列中取出并触发执行。
嗅探核心基础:运算符in
三、嗅探核心基础——运算符in
该方法利用运算符in遍历命名空间中的方法,若能获取到值,则代表可执行;反之,则代表不可执行。

运算符in
通过此例,即可理解Sniffer.js的嗅探原理。
抽象嗅探方法
四、抽象出嗅探方法
/**
* @function {private} 检测方法是否可用
* @param {string} funcName -- 方法名***.***.***
* @param {object} base -- 方法所依附的对象
**/
function checkMethod(funcName, base){
var methodList = funcName.split('.'), // 方法名list
readyFunc = base, // 检测合格的函数部分
result = {
'success':true,
'func':function(){}
}, // 返回的检测结果
methodName, // 单个方法名
i;
for(i = 0; i < methodList.length; i++){
methodName = methodList[i];
if(methodName in readyFunc){
readyFunc = readyFunc[methodName];
}else{
result.success = false;
return result;
}
}
result.func = readyFunc;
return result;
}
对于Wall.message.setName('wang', 'wall');这类方法,判断其是否可执行需执行以下步骤:
1. 判断Wall是否存在于window对象中。
2. 若Wall存在,则继续判断message是否在Wall中。
3. 若message存在,则继续判断setName是否在message中。
4. 最后,若所有层级均判断存在,则代表可执行。若中间任意一个检测不通过,则方法不可执行。
缓存机制实现
五、实现缓存
缓存使用闭包实现,以队列性质存储在list中。
;(function(FUN, undefined){
'use strict'
var list = []; // 存储订阅的需要调用的方法
// 执行方法
FUN.run = function(){
// 很多代码...
//将订阅的函数缓存起来
list.push(...);
};
})(window.Sniffer || (window.Sniffer = {}));
六、确定队列中单个项的内容
1. 指定检测的基点 base
由于运算符in工作时需要几个基点供其检测,因此第一个要有的项就是base。
2. 检测的字符类型的方法名 name
如Wall.message.setName('wang', 'wall');,若已指定基点{'base':Wall},则还需存储message.setName,即{'base':Wall, 'name':'message.setName'}。
3. 缓存方法的参数 args
如Wall.message.setName('wang', 'wall');,有两个参数('wang', 'wall'),需将其存储起来,即{'base':Wall, 'name':'message.setName', 'args':['wang', 'wall']}。
参数使用数组缓存,是因为方法参数是变化的,后续代码需通过apply触发执行。同理,这里的参数需用数组进行缓存。
因此,缓存队列的单个项内容如下:
{
'base':Wall,
'name':'message.setName',
'args':['wang', 'wall']
}
run方法实现
七、实现run方法
;(function(FUN, undefined){
'use strict'
var list = []; // 存储订阅的需要调用的方法
/**
* @function 函数转换接口,用于判断函数是否存在命名空间中,有则调用,无则不调用
* @version {create} 2015-11-30
* @description
* 用途:只设计用于延迟加载
* 示例:Wall.mytext.init(45, false);
* 调用:Sniffer.run({'base':window, 'name':'Wall.mytext.init'}, 45, false);
或 Sniffer.run({'base':Wall, 'name':'mytext.init'}, 45, false);
* 如果不知道参数的个数,不能直接写,可以用apply的方式调用当前方法
* 示例: Sniffer.run.apply(window, [ {'name':'Wall.mytext.init'}, 45, false ]);
**/
FUN.run = function(){
if(arguments.length < 1 || typeof arguments[0] != 'object'){
throw new Error('Sniffer.run 参数错误');
return;
}
var name = arguments[0].name, // 函数名 0位为Object类型,方便做扩展
subscribe = arguments[0].subscribe || false, // 订阅当函数可执行时,调用该函数, true:订阅; false:不订阅
prompt = arguments[0].prompt || false, // 是否显示提示语(当函数未能执行的时候)
promptMsg = arguments[0].promptMsg || '功能还在加载中,请稍候', // 函数未能执行提示语
base = arguments[0].base || window, // 基准对象,函数查找的起点
args = Array.prototype.slice.call(arguments), // 参数列表
funcArgs = args.slice(1), // 函数的参数列表
callbackFunc = {}, // 临时存放需要回调的函数
result; // 检测结果
result = checkMethod(name, base);
if(result.success){
subscribe = false;
try{
return result.func.apply(result.func, funcArgs); // apply调整函数的指针指向
}catch(e){
(typeof console != 'undefined') && console.log && console.log('错误:name='+ e.name +'; message='+ e.message);
}
}else{
if(prompt){
// 输出提示语到页面,代码略
}
}
//将订阅的函数缓存起来
if(subscribe){
callbackFunc.name = name;
callbackFunc.base = base;
callbackFunc.args = funcArgs;
list.push(callbackFunc);
}
};
// 嗅探方法
function checkMethod(funcName, base){
// 代码...
}
})(window.Sniffer || (window.Sniffer = {}));
run方法的作用是:检测方法是否可执行,若可执行则执行;若不可执行,则根据传入参数决定是否缓存。
该run方法的亮点在于巧妙利用arguments,实现0-n个参数自由传入。
第一个形参arguments[0],固定用于传入配置项。存储要检测的基点base,方法字符串argument[0].name以及缓存标志arguments[0].subscribe。
第二个形参到第n个形参,由方法调用者传入所需使用的参数。
利用泛型方法,将arguments转换为真正的数组。(args = Array.prototype.slice.call(arguments))
然后,切割出方法调用所需的参数。(funcArgs = args.slice(1))
run方法的arguments处理完毕后,即可调用checkMethod方法进行嗅探。
根据嗅探结果,分两种情况:
嗅探结果为可执行,则调用apply执行。
return result.func.apply(result.func, funcArgs);
此处的重点是指定作用域为result.func,即例子的Wall.message.setName。
这样,若方法中使用了this,指向也不会发生改变。
使用return,是因为部分方法执行后会有返回值,因此此处需加上return,将返回值传递出去。
嗅探结果为不可执行,则根据传入的配置值subscribe,决定是否缓存到队列list中。
若需缓存,则拼接好队列单个项,push进list。
trigger方法实现
八、实现trigger方法
;(function(FUN, undefined){
'use strict'
var list = []; // 存储订阅的需要调用的方法
// 执行方法
FUN.run = function(){
// 代码...
};
/**
* @function 触发函数接口,调用已提前订阅的函数
* @param {object} option -- 需要调用的相关参数
* @description
* 用途:只设计用于延迟加载
* 另外,调用trigger方法的前提是,订阅方法所在js已经加载并解析完毕
* 不管触发成功与否,都会清除list中对应的项
**/
FUN.trigger = function(option){
if(typeof option !== 'object'){
throw new Error('Sniffer.trigger 参数错误');
return;
}
var funcName = option.name || '', // 函数名
base = option.base || window, // 基准对象,函数查找的起点
newList = [], // 用于更新list
result, // 检测结果
func, // 存储执行方法的指针
i, // 遍历list
param; // 临时存储list[i]
console.log(funcName in base);
if(funcName.length < 1){
return;
}
// 遍历list,执行对应的函数,并将其从缓存池list中删除
for(i = 0; i < list.length; i++){
param = list[i];
if(param.name == funcName){
result = checkMethod(funcName, base);
if( result.success ){
try{
result.func.apply(result.func, param.args);
}catch(e){
(typeof console != 'undefined') && console.log && console.log('错误:name='+ e.name +'; message='+ e.message);
}
}
}else{
newList.push(param);
}
}
list = newList;
};
// 嗅探方法
function checkMethod(funcName, base){
// 代码...
}
})(window.Sniffer || (window.Sniffer = {}));
若前述run方法已理解,trigger方法便不难掌握。
1. 首先需告知trigger方法,应从队列list中取出哪个方法执行。
2. 在执行方法前,需再次嗅探该方法是否已存在。若存在,方可执行;否则,可认为方法已不存在,可从缓存中移除。
实用性与可靠度
九、实用性和可靠度
实用性方面毋庸置疑,无论何种代码栈,Sniffer.js都值得一试!
可靠度方面,Sniffer.js已应用于高流量公司产品,至今未收到任何兼容或性能问题的反馈。这方面也可打包票!
最后,附上源码地址:https://github.com/wall-wxk/sniffer/blob/master/sniffer.js
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持本站!
JavaScript怎么准确判断数据类型:typeof、instanceof与Object.prototype.toString
2026-11-05 17:09:21
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS运算符优先级与结合性的区别是什么?
2026-11-05 13:22:02
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
D3.js 与 ECharts 选型对比:灵活性 vs 开发效率
2026-11-05 09:45:01
