分析别人的源代码,除了可以了解程序功能是如何实现之外,还可以学到一些比较先进的编程方式和思想,进而提高自己的水平。本着这一想法,我将对QUnit的源代码加以解读,也希望对大家js水平的提高有个帮助作用。
好的js框架在语言上总是很干练的,里面也使用了很多比较先进的编程技巧,这就要求读者必须要有比较扎实的js基础知识。在这里我重点推荐汤姆大叔的译作《深入理解JavaScript系列》。文章很多共有50多篇,前20多篇对js基础知识作了很深入的讲解(市面上没看到比他更深入的书籍和博文,也可能是我看的资料少的缘故),后20多篇讲的是js的设计模式,我重点推荐前20多篇。相信通过对该系列重复的阅读(一次读不懂不怕,多读几次总会有感觉的),一定会让你的js水平有个质的提升。
现在我们言归正传,开始解读QUnit源代码。我们首先来看QUnit源码的大致结构:
(function( window ) { var QUnit, config, ...; function Test( settings ) { } Test.count = 0; Test.prototype = { }; QUnit = { module: function( name, testEnvironment ) { }, asyncTest: function( testName, expected, callback ) { }, test: function( testName, expected, callback, async ) { }, expect: function( asserts ) { }, start: function( count ) { }, stop: function( count ) { } }; QUnit.assert = { ok: function( result, msg ) { }, equal: function( actual, expected, message ) { }, notEqual: function( actual, expected, message ) { }, deepEqual: function( actual, expected, message ) { }, notDeepEqual: function( actual, expected, message ) { }, strictEqual: function( actual, expected, message ) { }, notStrictEqual: function( actual, expected, message ) { }, "throws": function( block, expected, message ) { } }; extend( QUnit, QUnit.assert ); ... if ( typeof exports === "undefined" ) { extend( window, QUnit ); window.QUnit = QUnit; } ... QUnit.load = function() { }; addEvent( window, "load", QUnit.load ); function addEvent( elem, type, fn ) { if ( elem.addEventListener ) { elem.addEventListener( type, fn, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, fn ); } else { fn(); } } function extend( a, b ) { for ( var prop in b ) { if ( b[ prop ] === undefined ) { delete a[ prop ]; // 因为设置window.constructor避免IE8发生 "Member not found" 的错误 } else if ( prop !== "constructor" || a !== window ) { a[ prop ] = b[ prop ]; } } return a; } ... function id( name ) { return !!( typeof document !== "undefined" && document && document.getElementById ) && document.getElementById( name ); } ... // 获取全局对象 }( (function() {return this;}.call()) ));
框架整体式一个即时匿名函数,也叫立即执行匿名函数。
(function(){ ... }(args))
他的特点是代码在解析之后会自动执行,本身又是一个闭包环境,内部变量不会对全局变量造成污染。这种方式是大多数第三方类库使用的开发方式,例如jquery,值得大家在自己的项目中实践。此外我们还注意到即使匿名函数传递的参数:
(function() {return this;}.call())
call方法执行时候的上下文是null,this会返回global,也就是返回window对象。具体的原因可以通过阅读博文《深入理解JavaScript系列(13):This? Yes,this!》找到答案。
代码之后定义了一些内部使用的变量。接下来定义的是Test对象,会在QUnit.test()中使用,稍后的文章我会加以介绍。
下来的内容就是重头戏了,他定义了QUnit常用的api方法和相关的断言方法。对于断言方法你也许会感到奇怪,因为他是定义在QUnit.assert中的。而我们在单元测试中使用的时候,前面并没有添加QUnit前缀,这是怎么回事呢。原因是源码中使用了扩展方法,把QUnit.assert中的方法扩展到了QUnit中。
extend( QUnit, QUnit.assert );
扩展方法位于稍后的位置,我们来看他是如何实现的。
function extend( a, b ) { for ( var prop in b ) { if ( b[ prop ] === undefined ) { delete a[ prop ]; // 因为设置window.constructor避免IE8发生 "Member not found" 的错误 } else if ( prop !== "constructor" || a !== window ) { a[ prop ] = b[ prop ]; } } return a; }
可以说这个方法实现的中规中矩,这是一种很通用的实现扩展或者继承的实现方式。就是简单的把b中存在的属性复制给了a,函数最后然后返回a对象。extend( QUnit, QUnit.assert ) 实现的功能,相信大家一定已经清楚了。源码中很多实现扩展的地方都使用了这个方法。
接下来QUnit使用下面的语句把本身暴露给了window对象,这样我们才能在单元测试中访问到api相关的方法。例如在单元测试中,我们可以直接使用module和test方法,前面不用添加QUnit前缀,就是下面的代码起的作用。QUnit把这些属性复制给了window。你直接使用的module和test其实就是window.module 和 window.test。
if ( typeof exports === "undefined" ) { extend( window, QUnit ); window.QUnit = QUnit; }
源码中定义了事件注册的方法:addEvent()。代码中使用addEvent( window, "load", QUnit.load )实现对window load事件的绑定,当页面加载完毕之后执行QUnit对象的加载操作。
QUnit.load = function() { }; addEvent( window, "load", QUnit.load );