一、热身——先看实战代码
a.js 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ;(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文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <script type='text/javascript'> <% out.print("Sniffer.run({'base':window,'name':'Wall.say','subscribe':true}, 'wall');\n"); %>
$LAB.script("a.js").wait(function(){ Sniffer.trigger({ 'base':window, 'name':'Wall.say' }); }); </script>
|
这样,不管a.js文件多大,Wall.say('wall')
都可以等到文件真正加载完后,再执行。
二、工具简介
1 2 3 4 5 6
| Sniffer.run({ 'base':Wall, 'name':'message.setName', 'subscribe':true }, 'wang', 'wall');
|
看这个执行代码,你也许会感觉困惑-什么鬼!
sniffer.js作用就是可以试探执行方法,如果不可执行,也不会抛错。
比如例子Wall.message.setName('wang', 'wall');
如果该方法所在文件还没有加载,也不会报错。
处理的逻辑就是先缓存起来,等方法加载好后,再进行调用。
再次调用的方法如下:
1 2 3 4 5
| 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
去遍历命名空间中的方法,如果取得到值,则代表可执行。反之,则代表不可执行。
通过这个例子,就可以知道这个sniffer.js的嗅探原理了。
四、抽象出嗅探方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
function checkMethod(funcName, base){ var methodList = funcName.split('.'), 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
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ;(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
。所以要存储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
去做触发。同理,这里的参数就需要用数组进行缓存
所以,缓存队列的单个项内容如下:
1 2 3 4 5
| { 'base':Wall, 'name':'message.setName', 'args':['wang', 'wall'] }
|
七、实现run方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| ;(function(FUN, undefined){ 'use strict'
var list = [];
FUN.run = function(){ if(arguments.length < 1 || typeof arguments[0] != 'object'){ throw new Error('Sniffer.run 参数错误'); return; } var name = arguments[0].name, subscribe = arguments[0].subscribe || 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); }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方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| ;(function(FUN, undefined){ 'use strict'
var list = [];
FUN.run = function(){ };
FUN.trigger = function(option){ if(typeof option !== 'object'){ throw new Error('Sniffer.trigger 参数错误'); return; } var funcName = option.name || '', base = option.base || window, newList = [], result, func, i, param; if(funcName.length < 1){ return; } 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