create: 2013-08-30 17:26
Qatrix 是一个非常轻量级的库,官方的宣传口号是“用更少的代码来构建高性能的应用”,那么有多高效呢,还是去源码里一探究竟吧。
官网地址:http://qatrix.com/
Github:https://github.com/qatrix/Qatrix
目录
根据文档描述,整个库可以分为如下几个部分:
- Selectors
- Utilities
- Animation
- AJAX
- CSS
- Event
- Manipulation
- Cookie
- String
- Attributes
- Data
- Storage
- Miscellaneous
我可能会根据实际的阅读调整顺序。
概览
先从整体上观察一下 Qatrix。
(function(window, document, undefined) { var ... Qatrix = { ... };
for (var fn in Qatrix) {
window[fn] = Qatrix[fn];
}
Qatrix.version = version;
window.Qatrix = Qatrix;
...
if (typeof define === 'function' && define.amd) {
define('qatrix', [], Qatrix);
}
})(window, document);
这里采用了很经典的立即执行函数(immediately invoked function expression)方式来封装代码,和 jQuery 一样,不多说。
在函数内部声明了一个变量 Qatrix 作为命名空间,随后将 Qatrix 上的所有方法暴露赋值给了 window 对象,然后又将 Qatrix 赋值给 window 上的同名变量,这样,Qatrix 的所有 API 均可以通过两种方式调用:fn() 或 Qatrix.fn()。
最后一句提供了简单的 AMD 支持,对于 AMD 的知识就不在这里提了。
基础方法
$each
有些方法例如 $each 会被其他方法依赖,属于很底层,因此提前说说。
$each: function (haystack, callback) { var i = 0, length = haystack.length, type = typeof haystack, is_object = type === 'object', name;
if (is_object && (length - 1) in haystack) {
for (; i < length;) {
if (callback.call(haystack[i], i, haystack[i++]) === false) {
break;
}
}
}
else if (is_object) {
for (name in haystack) {
callback.call(haystack[name], name, haystack[name]);
}
}
else {
callback.call(haystack, 0, haystack);
}
return haystack;
}
整个 $each 就是很常见的 forEach 方法,主要用于遍历对象、数组的元素,但这个方法内部并没有明确的区分数组,它采用了一种取巧的方式,即如果一个元素是对象(数组也是对象),并且对象的 length 属性减去 1 存在于对象上,那么就可以认为该对象为数组(或类数组,即对象的属性名均为数字)。
mapcall
和 $each 类似,只不过 mapcall 是内部方法,只用来循环数组,功能比较单一。
Selectors
先来看选择器。Qatrix 共提供了 6 个方法来选择元素。
$
这个 $ 太简单了,就是一句话:
$: function (id) { return document.getElementById(id); }
$id
$id: function (id, callback) { var match = [], elem;
$each(id instanceof Array ? id : id.split(' '), function (i, item) {
elem = $(item);
if (elem !== null) {
match.push(elem);
}
});
return callback ? mapcall(match, callback) : match;
}
前面提到的 $ 只能选择一个元素,而这个 $id 的功能比 $ 强大些,你可以传入一个数组来选择多个元素。整个方法内部使用了前面提到过的 $each 和 mapcall 两个方法,逻辑不复杂,不多说了。
$tag
调用了 Element.getElementsByTagName() 方法。
$class
当我们要处理浏览器兼容问题时,常常会去判断某个方法是否存在,如果该方法不存在,则使用替代方法,例如浏览器 A 支持方法 f1,浏览器 B 支持替代方法 f2,我们可以提供一个通用的 API 来处理兼容问题,
var f = f1 ? function() { return f1(); } : function() { return f2(); };
$class 在这里就是采用的该技巧,首先判断 getElementsByClassName 方法是否存在,如果不存在,则使用替代方法,而这个替代方法就是先获取所有元素,然后通过正则表达式来进行匹配。
$dom
略过。
$select
因为 Qatrix 要保证文件的小巧,因此它不可能往代码里塞进去一个像 Sizzle 那样的 CSS 选择器引擎,对于高级浏览器,可以使用 querySelectorAll() 方法,那么对于不支持该方法的 IE 6/7 呢? Qatrix 使用了一个非常聪明的办法。
一般的 CSS 选择器引擎会去分析你传入的 CSS 选择器,然后在 DOM 树中查找匹配的元素,Qatrix 也才用了同样的思路,只不过呢,它选择了让浏览器去解析 CSS 选择器。
例如,我们现在有这么一个 HTML 结构:
<div class="wrapper"> <div class="content1"> <div class="nav"> <div class="first"> Hi, I'm the one! </div> </div> </div> <div class="content2"> <div class="nav"> <div class="first"> Hi, I'm another one! </div> </div> </div> </div>
我想选择其中两个 class 为 first 的 div,当然我可以直接使用 $class,但我决定折腾一下:
var el = $select(".nav > div.first");
让我们来写实现一下 $select:
function $select(selector) { var match = []; var style = document.createElement('style'); document.body.appendChild(style);
var s = style.styleSheet;
s.addRule(selector, 'q:a');
var els = document.getElementsByTagName('*');
for(var i = 0, len = els.length; i < len; i++) {
if(els[i].currentStyle.q == 'a') {
match.push(els[i]);
}
}
return match;
}
首先,我们创建了一个 style 元素,接着将它附加到 body 上,随后获取了 style 元素的 styleSheet 属性。
http://msdn.microsoft.com/library/ie/ms535871.aspx 为 styleSheet 对象的官方文档。
简单来说,styleSheet 对象对应了页面中以 style、link 或 @import 引入的样式表,对象提供了一些方法来操作样式规则,而我们上面的代码中使用了 addRule,这个方法就是创建一条 CSS 规则,我们传入 CSS 选择器和一个 "q:a","q:a" 其实可以为任意值,只要符合 CSS 值的规范即可,这样,当这条规则插入后,浏览器自然就会对其进行解析,然后应用到对应的元素上,也就是说,现在页面中符合 ".nav > div.first" 这条选择器的元素已经获得了一个样式规则 "q:a"。
接下来,我们循环页面中的所有元素,一一检查它们是否拥有设定的样式,在这里使用了 IE 独有的对象 currentStyle,通过比对该对象上的 q 属性值是否为 a,来确定当前的元素是否符合我们的要求。
使用浏览器来解析选择器,速度必然没有问题,但是弊端很明显,IE 6、7 对于 CSS 选择器的支持很有限,所以使得这个方法的作用大打折扣,不过就像作者在文档中说的,还是使用 id 与 class,而不是构造复杂的选择器。
真实的 $select 与我的实现略有差异,不过,理解思路就好了。
OK,选择器就讲到这里啦。
Utilities
工具类中有三个方法,其中 $each 在上面见过了,来看其他两个。
$require
这个方法用于http://developer.yahoo.com/异步加载脚本和样式文件,并提供了回调函数,可以在文件加载完毕后执行。
该方法值得说的就是如何去判断文件加载完毕,由于浏览器间存在差异(毫无疑问),所以常见的办法就是同时监听两个事件:load 和 readystatechange,对于 load 事件来说,事件触发,表明文件加载完毕,而对于 readystatechange 事件,它设置了多个状态值,例如 "loading" 表示文件正在加载,"interactive" 表示解析完毕,但还在加载子资源,"complete" 表示文件加载结束。
item.onload = item.onreadystatechange = function (event) { if (event.type === 'load' || (/loaded|complete/.test(item.readyState))) { queue.splice(queue.indexOf(src), 1); if (queue.length === 0 && callback) { callback(); } } };
事件处理函数中判断了 loaded 和 complete 两个状态,这个……又是由于兼容性问题。
不过作为编写代码的好习惯,无用的事件处理函数要尽快清除掉,上面的代码中,load 或 readystatechange 事件触发后,这个事件处理函数就没有用了,所以应该再加入一行代码:
item.onload = item.onreadystatechange = null;
$template
Qatrix 提供了一个模板引擎,能够解析类似于 Mustache 语法的模板。
$template: function (template, data) { var content = template_cache[template];
if (!content) {
content = "var s='';s+=\'" +
template.replace(/[\r\t\n]/g, " ")
.split("'").join("\\'")
.split("\t").join("'")
.replace(/\{\{#([\w]*)\}\}(.*)\{\{\/(\1)\}\}/ig, function (match, $1, $2) {
return "\';var i=0,l=data." + $1 + ".length,d=data." + $1 + ";for(;i<l;i++){s+=\'" + $2.replace(/\{\{(\.|this)\}\}/g, "'+d[i]+'").replace(/\{\{([\w]*)\}\}/g, "'+d[i].$1+'") + "\'}s+=\'";
})
.replace(/\{\{(.+?)\}\}/g, "'+data.$1+'") +
"';return s;";
template_cache[template] = content;
}
return data ? new Function("data", content)(data) : new Function("data", content);
}
这个引擎的思路就是将模板字符串转成 JavaScript 代码字符串,然后使用 Function 来解析这段代码。
new Function(param1, param2, ..., content)
使用 Function 可以将字符串转为 JavaScript 函数,参数列表中最后一个参数为函数体,其余参数均为新生成的函数的参数名。在 Qatrix 这段代码中,最后解析的字符串便是函数体,而参数则为 data,用于提供数据。
来分析下解析模板的过程,这段代码掺杂了字符串替换、分解、合并等诸多操作,看起来不是很容易。所以还是举一个例子,来逐步的分析代码的执行过程。
var template = "<div>\ <h1>{{header}}</h1>\ <h2>{{header2}}</h2>\ <h3>{{header3}}</h3>\ <h4>{{header4}}</h4>\ <h5>{{header5}}</h5>\ <h6>{{header6}}</h6>\ <ul>\ {{#list}}\ <li>{{this}}</li>\ {{/list}}\ </ul>\ <ul>\ {{#people}}\ <li>{{name}} - {{city}}</li>\ {{/people}}\ </ul>\ </div>";
这段代码来源于官网的文档,我增加了一些换行,可以看得更清楚一些。我们先看源码中对于 template 的操作:
template.replace(/[\r\t\n]/g, " ")
这一句将回车、换行、制表符等字符转为一个空格,经过这个操作后,模板就变成了下面这个样子:
<div> <h1>{{header}}</h1> <h2>{{header2}}</h2> <h3>{{header3}}</h3> <h4>{{header4}}</h4> <h5>{{header5}}</h5> <h6>{{header6}}</h6> <ul> {{#list}} <li>{{this}}</li> {{/list}} </ul> <ul> {{#people}} <li>{{name}} - {{city}}</li> {{/people}} </ul> </div>
接下来的操作是:
.split("'").join("\'") .split("\t").join("'")
第一句是将模板按照单引号分组,然后又按照转义的单引号合并。而第二句和第一句类似,是将制表符转为单引号。(制表符 \t 已经在上面转成空白了,这里为什么又操作了一次?)
呃,由于选择的这个模板的问题,经过上面的操作后,模板字符串没有变化。继续往下看,
.replace(/{{#([\w])}}(.){{\/(\1)}}/ig, function (match, $1, $2) { return "\';var i=0,l=data." + $1 + ".length,d=data." + $1 + ";for(;i<l;i++){s+=\'" + $2.replace(/{{(.|this)}}/g, "'+d[i]+'").replace(/{{([\w]*)}}/g, "'+d[i].$1+'") + "\'}s+=\'"; })
先看这个正则表达式,
/{{#([\w])}}(.){{\/(\1)}}/
把转义内容还原下,再调整下位置,
{{#([\w])}} (.) {{/(\1)}}
有些清晰了吧,这是在匹配模板中 {{#list}} 与 {{/list}} 中间的内容,因为在 mustache 语法中,被这两个标签包裹的内容是用于循环的。
replace 方法的第二个参数可以是一个函数,该函数的参数为:匹配的字符串,捕获组1,捕获组2……它的返回值会替代匹配的字符串。对于这块字符串,
{{#list}} <li>{{this}}</li> {{/list}}
match 为整个字符串,$1 为第一个匹配组:list,$2 为第二个匹配组:<li>{{this}}</li> 也就是循环体。
上面说到,这条语句匹配的内容是用来循环的,因此代码在这里将这部分内容转为了 for 循环。下面就是其中的部分结果,我去掉了多余的空格,调整了格式:
<ul>'; var i=0,l=data.list.length,d=data.list; for(;i<l;i++){ s+='<li>'+ d[i] + '</li>' } s+='</ul><ul>'; var i=0,l=data.people.length,d=data.people; for(;i<l;i++){ s+='<li>'+d[i].name+' - '+d[i].city+'</li>' } s+='</ul></div>
在模板语法中,{{this}} 或 {{.}} 表示为当前循环的条目,如果是 {{xxx}} 则表明 xxx 是当前循环条目的属性,前面说了,最终生成的函数会传入一个名为 data 的参数,该对象用于提供数据。主要要注意的是单引号和字符串拼接的处理。
最后一句 replace 是用来替换那些不属于循环的内容,例如 {{header}} 这些。它们就是 data 的属性:
<div> <h1>'+data.header+' </h1> <h2>'+data.header2+' </h2> <h3>'+data.header3+' </h3> <h4>'+data.header4+' </h4> <h5>'+data.header5+' </h5> <h6>'+data.header6+' </h6> <ul>'; var i=0, l=data.list.length, d=data.list; for(;i<l;i++){ s+='<li>'+d[i]+'</li>' } s+='</ul><ul>'; var i=0, l=data.people.length, d=data.people; for(;i<l;i++){ s+='<li>'+d[i].name+' - '+d[i].city+'</li>' } s+='</ul></div>
看,内容基本上替换完毕,再加上前面变量 s 的声明,和最后 return s; 这句,整个模板解析便结束了。