两个问题
1.jQuery的链式操作是如何实现的?
2.为什么要用链式操作?
大家认为这两个问题哪个好回答一点呢?
链式操作
原理相信百度一下一大把,实际上链式操作仅仅是通过对象上的方法最后
return this
把对象再返回回来,对象当然可以继续调用方法啦,所以就可以链式操作了。那么,简单实现一个:
// 定义一个JS类 function Demo() { } // 扩展它的prototype Demo.prototype = { setName: function (name) { this .name = name; return this ; }, getName: function () { return this .name; }, setAge: function (age) { this .age = age; return this ; } }; // //工厂函数 function D() { return new Demo(); } // 去实现可链式的调用 D().setName("CJ").setAge(18).setName();
但……为什么要用呢?
一般的解释:
节省代码量,代码看起来更优雅。
例如如果没有链式,那么你可能需要这样写代码:
document.getElementById("ele"
).dosomething();
document.getElementById(
"ele").dootherthing();
这个代码中调用了两次document.getElementById来获取DOM树的元素,这样消耗比较大,而且要写两行,而链式只要写一行,节省了代码……
但我们也可以用缓存元素啊。比如:
var ele = document.getElementById("ele" ); ele.dosomething(); ele.dootherthing();
而且两行并没有比一行多多少代码,甚至相应的封装反而使得代码更多了。
最糟糕的是所有对象的方法返回的都是对象本身,也就是说没有返回值,这不一定在任何环境下都适合。
举个例子,我们想弄一个超大整数BigInteger(意思是如果用Javascript的Number保存可能会溢出的整数),顺便扩展他的运算方法,会适合用链式操作么?
例如运算3 1415926535 * 4 - 271828182,如果设计成链式风格的方法可能会是这样的:
var result = ( new BigInteger("31415926535")).multiply( new BigInteger("4")).subtract( new BigInteger("271828182" )).val(); console.log( "result == " + result);
这看起来似乎也很优雅,但是如果我们想要中间的结果怎么办呢?或许会写成这样:
var bigInteger = new BigInteger("31415926535" ); var result1 = bigInteger.multiply( new BigInteger("4" )).val(); var result2 = bigInteger.subtract( new BigInteger("271828182" )).val(); console.log( "result1 == " + result1 + ", result2 == " + result2);
这似乎一点也不优雅了,和不用链式操作没啥不同嘛!
那么如果要求是原来的BigInteger不能改变呢?好吧,链式操作似乎不能满足这个需求了。
jQuery专注于DOM对象操作,而DOM的操作会在页面上体现,不需要在Javascript中通过返回值来表示,但计算操作却不一样,我们很可能需要通过Javascript返回中间过程值另作他用。
在设计的时候,我们需要考虑链式带来的好处和坏处,因为别人用了链式,所以就用链式,可能并不是一个很好的方案。
那么到底为什么要用链式操作呢?
为了更好的异步体验
Javascript是无阻塞语言,所以他不是没阻塞,而是不能阻塞,所以他需要通过事件来驱动,异步来完成一些本需要阻塞进程的操作。
但是异步编程是一种令人疯狂的东西……运行时候是分离的倒不要紧,但是编写代码时候也是分离的就……
常见的异步编程模型有哪些呢?
- 回调函数
所谓的回调函数,意指先在系统的某个地方对函数进行注册,让系统知道这个函数的存在,然后在以后,当某个事件发生时,再调用这个函数对事件进行响应。
function f(num, callback){ if (num<0 ) { alert( "调用低层函数处理!" ); alert( "分数不能为负,输入错误!" ); } else if (num==0 ){ alert( "调用低层函数处理!" ); alert( "该学生可能未参加考试!" ); } else { alert( "调用高层函数处理!" ); setTimeout(function(){callback();}, 1000); } }
这里callback则是回调函数。可以发现只有当num为非负数时候callback才会调用。
但是问题,如果我们不看函数内部,我们并不知道callback会几时调用,在什么情况下调用,代码间产生了一定耦合,流程上也会产生一定的混乱。
虽然回调函数是一种简单而易于部署的实现异步的方法,但从编程体验来说它却不够好。
- 事件监听
也就是采用事件驱动,执行顺序取决于事件顺序。
function EventTarget(){ this .handlers = {}; } EventTarget.prototype = { constructor: EventTarget, addHandler: function (type, handler){ this .handlers[type] = []; }, fire: function (){ if (! event.target){ event.target = this ; } if ( this .handlers[event.type instanceof Array]){ var handlers = this .handlers[event.type]; for ( var i = 0, len = handlers.length, i < len; i++ ){ handlers[i](event); } } }, removeHandler: function (type, handler){ if ( this .handlers[type] instanceof Array){ var handlers = this .handlers[type]; for ( var i = 0, le = handlers.length; i < len; i++ ){ if (handlers[i] === handler){ break ; } } handlers.splice(i, 1 ); } } };
上面是《JavaScript高级程序设计》中的自定义事件实现。于是我们就可以通过addHandler来绑定事件处理函数,用fire来触发事件,用removeHandler来删除事件处理函数。
虽然通过事件解耦了,但流程顺序更加混乱了。
- 链式异步
个人觉得链式操作最值得称赞的还是其解决了异步编程模型的执行流程不清晰的问题。jQuery中$(document).ready就非常好的阐释了这一理念。DOMCotentLoaded是一个事件,在DOM并未加载前,jQuery的大部分操作都不会奏效,但jQuery的设计者并没有把他当成事件一样来处理,而是转成一种“选其对象,对其操作”的思路。$选择了document对象,ready是其方法进行操作。这样子流程问题就非常清晰了,在链条越后位置的方法就越后执行。
( function (){ var isReady= false ; // 判断onDOMReady方法是否已经被执行过 var readyList= []; // 把需要执行的方法先暂存在这个数组里 var timer; // 定时器句柄 ready= function (fn) { if (isReady ) fn.call( document); else readyList.push( function () { return fn.call( this );}); return this ; } var onDOMReady= function (){ for ( var i=0;i<readyList.length;i++ ){ readyList[i].apply(document); } readyList = null ; } var bindReady = function (evt){ if (isReady) return ; isReady = true ; onDOMReady.call(window); if (document.removeEventListener){ document.removeEventListener( "DOMContentLoaded", bindReady, false ); } else if (document.attachEvent){ document.detachEvent( "onreadystatechange" , bindReady); if (window == window.top){ clearInterval(timer); timer = null ; } } }; if (document.addEventListener){ document.addEventListener( "DOMContentLoaded", bindReady, false ); } else if (document.attachEvent){ document.attachEvent( "onreadystatechange", function (){ if ((/loaded|complete/ ).test(document.readyState)) bindReady(); }); if (window == window.top){ timer = setInterval( function (){ try { isReady ||document.documentElement.doScroll('left'); // 在IE下用能否执行doScroll判断dom是否加载完毕 } catch (e){ return ; } bindReady(); }, 5 ); } } })();
上面的代码不能用$(document).ready,而应该是window.ready。
- Deferred & Promise
CommonJS中的异步编程模型也延续了这一想法,
每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。
所以我们可以这样写:
f1().then(f2).then(f3);
这种方法我们无需太过关注实现,也不太需要理解异步,只要懂得通过函数选对象,通过then进行操作,就能进行异步编程。
参考资料