【Javascript】Fixie.js——自动填充内容的插件

Fixie.js说明

Fixie.js是一个自动填充HTML文档内容的开源工具

官方网址地址:http://fixiejs.com/

为什么使用Fixie?

当我们设计网站的时候,由于无法确定最终填充的内容,经常需要添加一些lorem ipsum(关于Lorem ipsum)到文档以便预览一下文档的展现效果。

问题来了,添加过多无聊的内容,使得我们的HTML文档变得臃肿,并且陷入复制-粘贴,手工编辑的毅种循环中。

Fixie.js就是为解决这个问题而诞生的——通过解析语义化的HTML5标签,Fixie可以自动填充匹配标签元素类型的内容,使得我们的HTML文档简洁,测试高效。

使用说明

第一步:添加fixie.js 到文档中

在body结束标签之前添加

1
<script type="text/javascript" src="fixie.js"></script>

第二步:填充内容,这里有两种方法:

  1. 任何需要填充内容的位置,设置class="fixie",比如,如果先填充p标签的内容,直接设置<p class="fixie"></p>就行了,over,就这么简单。
  2. 通过fixie.init填充内容

通过CSS选择器选择待填充的元素,在console(控制台啊,亲)或者script标签里面执行

1
fixie.init([".array", "#of > .selectors", ".that", ".should", "#be > .populated", ".with", ".lorem"]) 

或者

1
fixie.init(".string, #of > .comma, .separated, .selectors, .that, .should, #be > .populated, .with, .lorem")

命令,就可以自动填充了

PS:执行

1
fixie.init(':empty')

可以填充文档里所有的空元素;

支持的元素

Fixie是通过标签类型来决定填充的内容的(语义化的体现),这里有几类是需要了解的。

- `<h1 class="fixie"></h1>` - 添加几个单词的文本,`h2 - h6`亦然。
- `<p class="fixie"></p>` - 填充一段文字
- `<article class="fixie"></article>` - 填充几段文本(几个段落)
- `<section class="fixie"></section>` - 同上
- `<img class="fixie"></img>` - 填充一张注明了尺寸的图片
- `<a class="fixie"></a>` - 填充一个随机的链接(做广告嫌疑?)

提示

修改默认的图片填充

执行 fixie.setImagePlaceholder(source).

比如,如果你想从Flickr下载图片来填充,可以执行

1
fixie.setImagePlaceholder('http://flickholdr.com/${w}/${h}/canon').init();

给容器添加 class=”fixie”

容器内部所有的非空后代元素(注意后代与子代的区别)都会受到影响

看下面的说明

1
2
3
<div class="fixie">
<p>Hello <a></a></p>
</div>

Fixie会保留P标签里面的”Hello”文本,但是会填充a标签里面的内容

Fixie for Rails

fixie-rails

突出填充内容,可以添加

1
.fixie{ border:4px solid red; }

到CSS里面,以便区分填充内容与真实内容。

授权

废话,不翻译了。

示例说明:

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
<!DOCTYPE html>
<html>
<head>
<title>Fixie.js Sample</title>

<style>
body{
font-family: Helvetica, arial, sans-serif;
width:800px;
margin:150px auto;
}
img{
width:400px;
height:300px;
float:right;
margin:20px;
}
.fixie{ color: red;}
</style>

</head>
<body>
<article>
<h1 class="fixie"></h1><!--这里会填充标题-->
<p> Check us out at <a class="fixie"></a>,<!--这里会填充连接,但是外部的P标签因为非空,所以不会受影响-->
and don't forget to view source.</p>
<section class="fixie"><!--section的后代元素都会填充-->
<p></p>
<img/>
<ul></ul>
<p></p>
<ol></ol>
</section>
<h2 class="fixie"></h2>
<section class="fixie"></section>
</article>

<script type="text/javascript" src="fixie.js"></script>
<script>
// Changes default image source to Flickr
fixie.setImagePlaceholder('http://flickholdr.com/${w}/${h}/fixie').init();
</script>
</body>
</html>

显示效果

1

Android 导致notifyDataSetChanged无效的一个错误

初学Android,发现有时候notifyDataSetChanged不起作用。后来发现是我理解错了。

一个典型的错误是:

1
2
3
4
5
list1 = new String[]{"listView1 item"};
ap1 = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,list1);
listView1.setAdapter(ap1);
list1 = new String[]{"new listView1 item"};
ap1.notifyDataSetChanged();

我一直以为ap1会监听list1的变化,重新初始化list1,然后执行相应的更新,现在才知道不对,ap1监听的是new String[]{“listView1 item”}的变化。

换种说法就是ap1本身会保存一个对原始数据源(new String[]{“listView1 item”})的内部引用inner_list1。

1
  list1 = new String[]{"new listView1 item"};

相当与切断了list1与原始数据源(new String[]{“listView1 item”})的关系,因此之后调用notifyDataSetChanged并不会起作用,因为list1 和inner_list1已经是存在于堆上的完全不同的两个对象了

错误回顾:

前段时间都是使用的Arrayist等等作为原始数据源,一般都是进行add之类的操作,所以list1 和inner_list1和一直都是保持对同以个变量的引用,
并没有出什么问题,当然,改为直接赋值还是会出问题。

看了一下Arrayadapter的源码:

ArrayAdapter:

1
2
3
public ArrayAdapter(Context context, int textViewResourceId, T[] objects) {
init(context, textViewResourceId, 0, Arrays.asList(objects));
}

Arrays:

1
2
3
public static <T> List<T> asList(T... array) {
return new ArrayList<T>(array);//注意这里的ArrayList不是常见的那个ArrayList,而是Arrays的一个内部类。。
}

所以上面的问题可以归结为这么个问题:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
String[] a = new String[] {"hello","world"};
String[] b = a;
List c = Arrays.asList(a);
a = new String[] {"hello","xesam"};
System.out.println(c.toString());//["hello","world"]
b[1] = "xesam";
System.out.println(c.toString());//["hello","xesam"]
````

下面是一个demo:


```java
package com.xesam.demo.listview;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;

public class TestDemoActivity extends Activity {

private Button button1;
private ListView listView1;
private Button button2;
private ListView listView2;
private Button button3;
private ListView listView3;

private String[] list1;
private String[] list2;
private String[] list3;
private String[] temp;

ArrayAdapter<String> ap1;
ArrayAdapter<String> ap2;
ArrayAdapter<String> ap3;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

button1 = (Button) findViewById(R.id.button1);
listView1 = (ListView) findViewById(R.id.listview1);
button2 = (Button) findViewById(R.id.button2);
listView2 = (ListView) findViewById(R.id.listview2);
button3 = (Button) findViewById(R.id.button3);
listView3 = (ListView) findViewById(R.id.listview3);

list1 = new String[]{"listView1 item"};
list2 = new String[]{"listView2 item"};
list3 = new String[]{"listView3 item"};
temp = list3;

ap1 = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,list1);
ap2 = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,list2);
ap3 = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,list3);

listView1.setAdapter(ap1);
listView2.setAdapter(ap2);
listView3.setAdapter(ap3);

button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
list1 = new String[]{"new listView1 item"};
ap1.notifyDataSetChanged();//无效
}
});
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
list2[0] = "new listView2 item";
ap2.notifyDataSetChanged();//有效
}
});
button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
temp[0] = "new listView3 item";
ap3.notifyDataSetChanged();//有效
}
});
}
}

【Javascript】手机滑动应用

完善版本参见Github

https://github.com/xesam/TouchSlide

简介

浏览器的动画效果一般都是用js来控制元素的 top,left,right,bottom 等属性来实现,不过在移动浏览器上,鉴于对css3的支持,完全可以抢先使用css3 translate。
不过需要注意的是,使用css translate在android上比较那个啥XX,在safari上,transalte2d的效果远远不如translate3d。
所以,移动浏览器上,最好是使用translate3d来实现。

手机滑动事件处理主要使用的是一个touch事件,在iOS上还有gusture事件,不过android现在还很悲剧。具体可以参考apple开发论坛,里面有详细说明。

当我们点击一个元素时,touchstart会最先触发,出发顺序:

touchstart —— mousedown —— click

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="test" style="width: 100%; height: 200px; background: red;"></div>
<div id="result"></div>
<script type="text/javascript">
var Time = {};
document.getElementById("test").addEventListener('touchstart',function(){
Time.t1 = (new Date()).getTime();
},false);
document.getElementById("test").addEventListener('mousedown',function(){
Time.t2 = (new Date()).getTime();
},false);
document.getElementById("test").addEventListener('click',function(){
Time.t3 = (new Date()).getTime();
document.getElementById("result").innerHTML = 'touchstart - mousedown = ' + (Time.t2 - Time.t1) + '<br />'
'mousedown - click = ' + (Time.t3 - Time.t2) ;
},false);
</script>

测试的各个浏览器版本越低,click相对 touchstart 的延迟越高

具体的滑动实现(一个swipe.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
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<style type="text/css">
*{margin: 0; padding: 0;}
body{ width: 100%;}
ul,li{ list-style: none;}
input[type="button"]{ margin: 10px; width: 40px; height: 24px;}
#page{ text-align: center;}
.demo{ width: 100%; height: 200px; overflow: hidden;}
.list{}
.list li{ display: table-cell; height: 200px; }
.list li:first-child{ background: #87ceeb;}
.list li:last-child{ background: #8b4513;}
</style>
</head>
<body>
<div id="page">
<div class="demo">
<ul class="list">
<li>1</li>
<li>2</li>
</ul>
</div>
<div class="demo">
<ul class="list">
<li>3</li>
<li>4</li>
</ul>
</div>
</div>
<script type="text/javascript">

window.TouchSlide = function(container){
if(!container){ //没有外包装,直接返回
return 1;
}
this.container = this._$(container);
this.element = this.container.children[0];
this.slides = this.element.children;
this.index = 0;
this.init();

var _this = this;

this.element.addEventListener('touchstart',function(e){
_this.touchstart(e);
},false);
this.element.addEventListener('touchmove',function(e){
_this.touchmove(e);
},false)
this.element.addEventListener('touchend',function(e){
_this.touchend(e);
},false)
window.addEventListener('resize', function(e){ //缩放屏幕的时候需要动态调整
_this.init();
}, false);
}
TouchSlide.prototype = {
constructor : TouchSlide,
_$ : function(el){
return 'string' == el ? document.getElementById(id) : el;
},
init : function(){
this.container.style.visibility = 'none';
this.width = this.container.getBoundingClientRect().width;
this.element.style.width = this.slides.length * this.width + 'px';
var index = this.slides.length;
while(index--){
this.slides[index].style.width = this.width + 'px';
}
this.container.style.visibility = 'visible';
},
slideTo : function(index, duration) {
this.move(0,index,duration);
this.index = index;
},
move : function(deltaX,index,duration){
var style = this.element.style;
style.webkitTransitionDuration = duration + 'ms';
style.webkitTransform = 'translate3d(' + ( deltaX - index * this.width) + 'px,0,0)';
},
isValidSlide : function(){
return Number(new Date()) - this.start.time < 250 && Math.abs(this.deltaX) > 20 //在250ms内滑动的距离超过20px
|| Math.abs(this.deltaX) > this.width/2 //或者滑动超过容器的一半宽度
},
isPastBounds : function(){
return !this.index && this.deltaX > 0 //第一个,但是依旧向右滑动
|| this.index == this.slides.length - 1 && this.deltaX < 0//最后一个,但是依旧向左滑动,这两种情况越界了,是无效的
},
touchstart : function(e){
var touchEvent = e.touches[0];
this.deltaX = 0;
this.start = {
x : touchEvent.pageX,
y : touchEvent.pageY,
time : Number(new Date())
} ;
this.isScrolling = undefined;
this.element.style.webkitTransitionDuration = 0;
},
touchmove : function(e){
this.deltaX = e.touches[0].pageX - this.start.x;
//判断是左右滑动还是上下滑动,上下滑动的话就无视
if(typeof this.isScrolling == 'undefined'){
this.isScrolling = !!( this.isScrolling || Math.abs(this.deltaX) < Math.abs(e.touches[0].pageY - this.start.pageY) );//判断是否是是竖直滚动
}
if(!this.isScrolling){
e.preventDefault();
this.deltaX = this.deltaX / (this.isPastBounds() ? 2 : 1);
}
this.move(this.deltaX,this.index,0);
},
touchend : function(e){
if (!this.isScrolling) {
this.slideTo( this.index + ( this.isValidSlide() && !this.isPastBounds() ? (this.deltaX < 0 ? 1 : -1) : 0 ), 200 );
}
}
}

Array.prototype.slice.call(document.getElementsByClassName('demo'),0).forEach(function(item){
new TouchSlide(item)
})

</script>
</body>
</html>

【Javascript】移动Native+HTML5混搭应用感受

说到HTML5在移动领域的开发,最推崇的估计就是跨平台与低门槛。同时又为了有native APP一样的效果,必须兼具原生特性,Titainium与Phonegap是两个比较典型的例子。

下面说的是我最近一段时间的开发感受以及尝试的几种方案:

第一种方案:

我一开始用的是phonegap + jq + jq mobile,从界面到效果都是JS撑起来的,最大的感受就是UI界面延迟无法忍受,不过如果只是发布为本地网站,那么一切都可以忍受,
因为用户对APP和网页的要求从来都不一样,不过这就不干phonegap鸟事了。

phonegap的优点:

  1. 基于JS和HTML5,没有门槛
  2. 跨平台,这个是废话。

phonegap的缺点:

  1. UI延迟厉害
  2. 兼容性,比如WEB SQL database在Android版本的问题,最后采用localstorage来模拟数据库。
  3. webview资源占用问题,以后就看apache基金会的造化了。

一句话总结:专用浏览器

第二种方案:

后来做完一个项目之后,开始采用另一个方案 Titainium。

Titainium 采用 js 来编写应用,而且JS符合部分 commonJs 规范。大致原理是:

android或者IOS API --> Titanium API--> JS组装。

Titanium的优点:

  1. JS作为一个胶水层,入门比较简单,1.8版本之后,引入了V8引擎,因此性能大幅提升。对最新的JS规范支持比较好,一些forEach方法可以调用。
  2. 各种组件都是基于原生的,因此性能和UI表现可以甩phonegap几条街。
  3. 部分跨平台,切换平台还是需要修改部分代码。
  4. 文档全,分类很清晰。

Titanium的缺点:

  1. 大!局限于Titianium的运行机制,这个问题暂时无解。同时也是因为引入了V8的引擎,所以什么代码都不写的话,编译生成的apk文件最少都有5M。
  2. 文档质量需要控制,有些文档根本就是错误的。
  3. 由于是重新封装了两个平台的API,所以Titanium的API也是很多的。但是由于中间添加了新的一层,当你需要去调用某些原生特性时,那就比较麻烦了。虽然说可以自己编写module,可是Titanium没有办法紧跟Android的版本,所以有关module这一块,从搭环境开始,就比较痛苦。我遇到的一个问题:三星某些机型阉割了google map服务,所以我要改用百度的地图,百度有API,所以我决定做一个module。但是从搭建Titanium module开发环境到开始写module,一天都没有成功。官方文档只是纯粹的堆砌,貌似根本就没有考虑实际情况。本来module是非常赞的一个特性【亮点】,但是不知道基于什么考虑,被官方弱化了。难道和他们的收费政策有关?最后地图问题用的JS版本地图+webview解决。每当此时,我就越发发现IOS的好了,而且真心话,Titanium更适合IOS一些。
  4. 用的人少,部分bug基本无解。
  5. 资源占用比较多,我的ME722,打开十几个window就挂掉了(这个做法不明智,我只是测试),换做view,超过20个一样延迟明显,最后使用数据栈勉强可以达到要求。

一句话总结:原型利器。

第三种方案:

原生APP + web内容。自己编写一个外壳,底部tab或者头部固定,外壳定制化。

为了避免庞大的jq,jqmobile或者jqmobi,我只能自己编写一个小型的库来处理,采用了jqmobile的思想,动画用CSS来实现,不过android2.2的CSS动画效果也是无法令人满意啊。
另外,最大的问题,外壳与内容的交互可能会比较复杂。这算是很接近phonegap的一种方案,但是相比phonegap,这种定制化能力更强,
有些头疼的问题(比如动态生成的页面中webview中滚动条的问题)可以采用别的方案(比如webview分割)来解决。

一句话总结:愚公移山。

上面是面向web开发人员的三种方案,总的来说,实现起来都比较简单,phonegap还可以很快的迁移到移动网站上。
但是测试工作量可以说是成倍增长,新版本周期短,升级快,bug产生的速度估计刚好抵消bug消除的速度。

但是,在这个JS和HTML5在移动领域还不够强大,并且缺乏杀手级应用的现在。一句话:“要把web app做得和native app一样,是不明智的”。
web app 就该是 web app 的样子,native app 就该是 native app 的样子,虽然我无法明确的告诉你他们到底应该是什么样子。

那么什么情况下应该用HTML5的方案,什么时候应该用原生的方案呢。

还是一句话:“it depends”,如果产品可以忍受UI和click的延迟以及组件奇怪的显示,多是内容获取而不涉及过多的交互,phonegap完全胜任。
如果需要原生的速度和显示,并且功能比较中庸不计较体积,那么Titanium可以胜任。
如果介于两者之间,那么可以自己做壳。前面的“如果”都是基于一个前提“团队初期或者人手不够”,如果真正要做一个好的应用的话,至少现阶段,还是原生搞起吧。

【Javascript】向后兼容的DOM事件绑定

主要内容

addEventListener绑定方式的问题:

1
2
3
4
document.body.addEventListener( 'click',
function() {
alert('body clicked');
},false);

以及第二个参数为object的优势:

1
2
3
4
5
6
7
document.body.addEventListener('click',
{
handleEvent: function() {
alert('body clicked');
}
},
false);

原文废话太多,我只翻译了主要部分。原文地址:原文 具体细节,请自行查看

先简短的看一下各个浏览器提供的DOM元素事件绑定接口:

1
2
3
4
5
6
//IE使用element.attachEvent:
document.body.attachEvent(
'onclick',
function() {
alert('body clicked');
});
1
2
3
4
5
6
7
//其他浏览器使用element.addEventListener:
document.body.addEventListener(
'click',
function() {
alert('body clicked');
},
false);

一般来说上面的第二个参数都是传入一个函数句柄,但是大多数javascript程序员并不知道第二个参数可以传入一对象obj【DOM2接口 http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventListener 】,如此一来,当event执行的时候,会隐式的调用obj的handleEvent方法。

1
2
3
4
5
6
7
8
document.body.addEventListener(
'click',
{
handleEvent: function() {
alert('body clicked');
}
},
false);

这么做的一个重要方面是 obj 的 handleEvent 只有在执行的时候才需要去访问【有点类似延迟绑定的效果】。同时,如果在两次event事件间隔中,handleEvent发生了改变,那么产生的效果会跟着改变。这样一个好处就是不用remove事件而直接切换事件。

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
document.body.addEventListener('click', obj, false);
// click body will error in some browsers because
// 现在还没有事件
obj.handleEvent = function() {alert('alpha');};

// 单击弹出alpha
obj.handleEvent = function() {alert('beta');};

// 单击弹出"beta"
document.body.removeEventListener('click', obj, false);
//单击什么都没有了

【注意,这个用法存在一定的兼容性问题。不过我现在只面向手机浏览器,所以请自行测试】

跨浏览器的事件绑定

对于事件绑定,各种库都会处理兼容问题,统一API,大都类似:

1
2
3
4
5
6
LIB_addEventListener(
document.body,
'click',
function() {
alert('body clicked');
});

个人实现的一个:

1
2
3
4
5
6
7
8
9
10
11
12
//一个封装
function LIB_addEventListener(el,type,fn){
el.addEventListener(type,fn,false);
}
function ViewObject() {
this.data = 'alpha';
LIB_addEventListener(document.body,'click',this.handleClick);
}
ViewObject.prototype.handleClick = function() {
console.log(this.data);
};
var test = new ViewObject();//单击弹出undefined

我们期望弹出‘alpha’,但是 this 指向 window,弹出的是 undefined;

解决方式,有的库添加了第四个参数,用来制定上下文:

1
2
3
4
5
6
7
8
9
10
11
function LIB_addEventListener(el,type,fn,obj){
el.addEventListener(type,fn.bind(obj),false);
}
function ViewObject() {
this.data = 'alpha';
LIB_addEventListener(document.body,'click',this.handleClick,this);
}
ViewObject.prototype.handleClick = function() {
console.log(this.data);
};
var test = new ViewObject();

此时我们弹出来‘alpha’

这种方式的问题:listener在绑定的时候就已经固定了,于是问题就又来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function LIB_addEventListener(el,type,fn,obj){
el.addEventListener(type,fn.bind(obj),false);
}
function ViewObject() {
this.data = 'alpha';
LIB_addEventListener(document.body,'click',this.handleClick,this);
}
ViewObject.prototype.handleClick = function() {
console.log(this.data);
};
var test = new ViewObject();
test.handleClick = function(){
console.log('new fn');//单击弹出的依旧是alpha。我们希望的是new fn
}

采用obj的绑定形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function LIB_addEventListener(el,type,obj){
el.addEventListener(type,obj,false);
}
function ViewObject() {
this.data = 'alpha';
LIB_addEventListener(document.body, 'click', this);
LIB_addEventListener(document.body, 'mousemove', this);
}
ViewObject.prototype.handleEvent = function(e) {
if('click' == e.type){
console.log(this.data)
}else{
console.log('not click')
}
};

var test = new ViewObject();

上面的例子我改了一下,但是可以说明问题。这样的灵活性也高一些。

【Javascript】轻量级Web Database存储库html5sql.js

阅读之前,先看W3C关于WEB Database的一段话:

Beware. This specification is no longer in active maintenance and the Web
Applications Working Group does not intend to maintain it further. 

意味着WEB Database规范陷入僵局。

html5sql

html5sql官方网址:http://html5sql.com/

简述

html5sql是一个让HTML5 Web Database使用更方便的轻量级JavaScript模块,它的基本功能是提供在一个事务中顺序执行SQL语句的结构。
虽然 Web Database并没有停止前进的脚步,这个模块仅仅可以简化与数据库的交互过程。这个模块还包含有其他很多小细节以便开发人员能够更简单,自然便捷的工作。

核心特征

  1. 提供以不同方式顺序执行SQL语句的能力:

     单条SQL语句
     一组SQL语句
     一组SQL语句对象(当你要想SQL中注入数据或者在SQL执行后调用一个回调函数时,可能需要使用这种形式)
     从一个分割完毕包含SQL语句的文件中
     
    
  2. 提供一个控制数据库版本的框架

例子

如果你用过HTML5 web database,你就会发现它有多蛋疼,尤其是当你开始建立你的表时。好了,现在你会发现这些都不是问题。为了更清楚的表明我的意思以及这个模块的能力,看下面的例子:

假设你打算建立一个表兵千插入一组数据到这个表里面。如果你使用html5sql的话,你只需要把所有的语句都放入一个单独的文件中,本例中我们取名为Setup-Tables.SQL。这个文件的形式类似于

本例中我们取名为Setup-Tables.SQL。这个文件的形式类似于:

1
2
3
4
5
6
7
8
9
CREATE TABLE example (id INTEGER PRIMARY KEY, data TEXT);
INSERT INTO example (data) VALUES ('First');
INSERT INTO example (data) VALUES ('Second');
INSERT INTO example (data) VALUES ('Third');

CREATE TABLE example2 (id INTEGER PRIMARY KEY, data TEXT);
INSERT INTO example2 (data) VALUES ('First');
INSERT INTO example2 (data) VALUES ('Second');
INSERT INTO example2 (data) VALUES ('Third');

有了html5sql之后,为了顺序执行这些SQL语句(包括创建表),你只需要打开数据库,然后添加下面一段代码就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$.get('Setup-Tables.SQL',function(sqlStatements){

html5sql.process(
//This is the text data from the SQL file you retrieved
sqlStatements,
function(){
// After all statements are processed this function
// will be called.
},
function(error){
// Handle any errors here
}
);
});

借助于jQuery的get方法,你已经从单独的文件(’Setup-Tables.SQL’)获得了SQL语句,并且按照出现的顺序分割执行了。

性能

上面的描述听起来还不错,不过你可能会问SQL顺序执行的时候性能是不是会受损。答案就是,影响不算明显,至少就我看来目前是这样。
比如,我用 Google的Chrome桌面版创建一张表,然后向表中顺序插入10,000条记录,整个执行过程的时间有波动,不过平均值处于2-6秒的范围内,
因此我有理由相信html5sql在处理大量数据的时候会有不俗的表现。

建议

SQL的核心被设计为一种顺序执行的语言,某些语句必须在其他的语句前面执行。比方说,在插入记录之前,你必须先创建一张表。
相反的,javascript 是一种异步、事件驱动的语言。对于开发者来说,这些异步特征增加了HTML5客户端数据库规范与说明的复杂性。
写这个库的时候,W3C已经不再维护Web SQL Database规范了。
尽管如此,由于webkit已经实现了设个接口,而且由于webkit内核浏览器的庞大用户群,尤其是移动设备上,因此这个库还是有用的。

虽然这个模块减少了HTML5 SQL database的复杂程度,但是并没有简化SQL本身,这是有意而为之的。SQL是一门强大的语言,盲目的简化它只会弄巧成拙。在我的经验中,学好SQL才是王道,大势所趋撒。

用户指南

html5sql模块中内建了3个函数:

一,html5sql.openDatabase(databaseName, displayName, estimatedSize)

html5sql.openDatabase是对原生openDatabase方法的一个封装,这个方法打开一个数据库连接并返回对连接的引用。这是所有其他操作之前的第一步。

这个方法有三个参数:

databaseName - 数据库的名字.任意你喜欢的有效的名字,通常可以是“com.yourcompany.yourapp”之类的
displayName - 数据库描述信息
estimatedSize - 数据库大小.5M = 5*1024*1024

如果读者熟悉web database原生方法的话,会发现上面的封装中少了版本信息这个参数。当你需要改变数据库的表结构时,版本信息是一个得力的标识工具,改变版本的这个方法被封装成html5sql的changeVersion方法。

现在,我们创建一个通常的数据库连接:

1
2
3
4
5
html5sql.openDatabase(
"com.mycompany.appdb",
"The App Database"
3*1024*1024
);

二,html5sql.process(SQL, finalSuccessCallback, errorCallback)

html5sql.process()方法是所有功能的载体,一旦你创建了数据库连接,就可以传递SQL语句,剩下的事情就交给html5sql.process(),他会保证SQL顺序执行。

html5sql.process()的第一个参数是SQL语句,其传递形式有许多种:

  1. 字符串形式 - 你可以向process方法传递一个简单字符串,形如:

     "SELECT * FROM table;"
     
    

    或者一组用分号(“;”)连接的简单字符串,形如:

     "CREATE table (id INTEGER PRIMARY KEY, data TEXT);" +
     "INSERT INTO table (data) VALUES ('One');" +
     "INSERT INTO table (data) VALUES ('Two');" +
     "INSERT INTO table (data) VALUES ('Three');" +
     "INSERT INTO table (data) VALUES ('Four');" +
     "INSERT INTO table (data) VALUES ('Five');"
    
  2. 来自独立文件的SQL语句。来自独立文件的SQL语句的例子跟上面的例子一样,没有本质区别。

  3. 一组SQL语句字符串,你可以向html5sql.process()传递一组SQL语句,形如:

      [
          "CREATE table (id INTEGER PRIMARY KEY, data TEXT);",
          "INSERT INTO table (data) VALUES ('One');",
          "INSERT INTO table (data) VALUES ('Two');",
          "INSERT INTO table (data) VALUES ('Three');",
          "INSERT INTO table (data) VALUES ('Four');",
          "INSERT INTO table (data) VALUES ('Five');"
      ]
    
  4. 一组SQL语句对象。这是一种最实用的形式,直接传递一组SQL语句对象。SQL语句对象的结构域原生executeSQL 方法的参数一致,有三个部分:

     SQL[string]——包含SQL语句的字符串,其中可以包含替换符“?”
     data[array]——一组需要插入到SQL语句中替换?符号的数据,其中SQL语句中?的数量必须与data中数据的数量一致
     success (function)——执行完SQL语句后回调的函数,可以处理SQL语句的执行结果。另外,如果这个方法返回一个数组,这个返回的数组还可以作为下一条SQL语句的data参数来执行。这样就允许你在SQL执行中调用将上一条SQL语句的结果,在使用外键的情况中,这个比较常见。
     
    

    或许最简单的定义以及使用这个对象的方式是只用对象字面量。通用的模板类似下面的形式:

          {
             "sql": "",
             "data": [],
             "success": function(transaction, results){
              //Code to be processed when sql statement has been processed
             }
          }
          
    

    因此,一个简单的SQL对象参数实例如下:

     [
         {
             "sql": "INSERT INTO contacts (name, phone) VALUES (?, ?)",
             "data": ["Joe Bob", "555-555-5555"],
             "success": function(transaction, results){
                     //Just Added Bob to contacts table
                 },
             },
         {
             "sql": "INSERT INTO contacts (name, phone) VALUES (?, ?)",
             "data": ["Mary Bob", "555-555-5555"],
             "success": function(){
                 //Just Added Mary to contacts table
             },
         }
     ]
    

  上面的对象中,唯一与原生executesql()不同的是,没有error部分的处理方式,这是因为有一个通用的错误处理回调函数来处理出现error的情况,从而避免每条语句都进行error定义。这个通用的错误处理函数就是 html5sql.process()的第三个参数。

小结

html5sql.process()总共有三个参数,

SQL - 上面叙述的任一种形式均可
finalSuccessCallback - 一个最终的,在所有SQL成功执行完毕后触发
errorCallback - 处理本过程中的所有错误的通用函数,发生任何错误时,当前事务会回滚,数据库版本不改变。

总结一下,这个方法的一个完整示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
html5sql.process(
[
"DROP TABLE table1",
"DROP TABLE table2",
"DROP TABLE table3",
"DROP TABLE table4"
],
function(){
console.log("Success Dropping Tables");
},
function(error, statement){
console.error("Error: " + error.message + " when processing " + statement);
}
);

三,html5sql.changeVersion(“oldVersion”,”newVersion”,SQL,successCallback,errorCallback)

html5sql.changeVersion() 是创建、迁移数据库以及处理版本所需要的方法。

这个方法会检测当前版本与旧的版本参数(oldVersion)是否一致,如果吻合的话,就会执行参数中的SQL语句,并改变数据库的版本为 newVersion参数所示的值。

oldVersion - 需要修改的数据库的版本号,默认初始值为""
newVersion - 你赋的新值
SQL - 你要执行的SQL语句.具体说明参见html5sql.process()部分
finalSuccessCallback - 成功执行后调用的函数
errorCallback - 通用数据处理函数,与html5sql.process()中一样, 发生错误的时候,整个事务回滚,并且不改变版本号。

源码深入

本JS库最大的问题就是如果要同时操纵多个数据库,那么就会引起混乱,这一点作者似乎并没有做多考虑。另外就是对于这种批量执行SQL语句的错误恢复处理感觉还是不够完善。

补充

补充一

html5sql.openDatabase()其实有四个参数

1
2
3
4
5
6
7
html5sql.openDatabase = function (name, displayname, size, whenOpen) {
html5sql.database = openDatabase(name, "", displayname, size);
readTransactionAvailable = typeof html5sql.database.readTransaction === 'function';
if (whenOpen) {
whenOpen();
}
};

最后的whenOpen是在获取数据库引用的时候触发的。

补充二

  另外还有两个用于调试的属性,logErrors和logInfo:默认都是false,设置为true的时候可以看到每一步操作的过程。由于调用的是控制台的console.log,可能在某些浏览器上引发错误。

补充三

对于process方法,不论你采用什么形式的SQL参数,最终都会被转换成SQL对象的形式。

当SQL语句包含的仅仅只有SELECT操作时,内部使用的是readTransaction方法,注意一下readTransaction与transaction的区别。

这边用的是readTransaction,这是为了保证不对表进行写操作,这是一种安全的举措,当然也可以用transaction。不过readTransaction方法存在一定的兼容性问题,所以使用之前应该保证检测无误。

【Javascript】轻量级JSON存储解决方案Lawnchair.js

Lawnchair

Lawnchair是一个轻量级的移动应用程序数据持久化存储方案,同时也是客户端JSON文档存储方法,优点是短小,语法简洁,扩展性比较好。

现在做HTML5移动应用除了LocalStorage的兼容性比较好之外,SQL web database以及IndexedDB都处在僵局中,虽然有人叫嚣着“我们应该干掉 LocalStorage API”,但那是后话,现在也没得选择。

Lawnchair有个曾经的官网:http://westcoastlogic.com/lawnchair/,不过这个站点提供的源码版本过时了,而且还有错误。

需要下载的话,最新版本在 https://github.com/brianleroux/lawnchair

应用示例【应用的是dom Storage】:

1
2
3
4
5
6
7
8
9
10
    var store = new Lawnchair({name:'testing'}, function(store) {
// 需要保存的对象
var me = {key:'brian'};
// 保存
store.save(me);

store.get('brian', function(me) {
console.log(me);
});
});

或者:

1
2
3
4
5
6
7
8
9
10
    var store = Lawnchair({name:'testing'}, function(store) {
// 需要保存的对象
var me = {key:'brian'};
// 保存
store.save(me);

store.get('brian', function(me) {
console.log(me);
});
});

因为使用了安全的构造函数,因此两种方法的效果一致。回调函数的第一个参数与返回的的store是同一个对象,在回调函数内部也可以用this代替。

初始化:

1
var store = new Lawnchair(option,callback);

option默认为空对象,有三个可选属性:

1
2
3
4
5
option = {
name://相当于表名
record://
adapter://存储类型
}

callback的第一个参数是当前对象,在回调函数内部也可以用this代替。

API

keys (callback) //返回存储对象的所有keys
save (obj, callback)//保存一个对象
batch(array, callback)//保存一组对象
get (key|array, callback)//获取一个或者一组对象,然后调用callback处理
exists (key, callback)//检查是否存在key,并将结果的布尔值(true/false)传递给callback函数
each(callback)//遍历集合,将(对象,对象索引)传递给callback函数
all (callback)//将所有对象放在一个数组返回
remove (key|array, callback)//移除一个或者一组元素。
nuke (callback)//销毁所有

初始化:

1
var store = new Lawnchair({name:'test'}, function() {});

或者

1
var store = new Lawnchair(function() {});

参数中必须有一个函数作为回调函数,哪怕是空。

1
2
3
4
5
6
7
8
9
10
11
save (obj, callback)//保存一个对象
var store = Lawnchair({name:'table'}, function(store) {
});
store.save({
key:'hust',
name:'xesam_1'
})
store.save({
key:'whu',
name:'xesam_2'
})

创建 Lawnchair 对象的时候,如果传入的 option 参数含有 name 属性,那么会创建一个类似 table._index_的数组用来保存索引值。

保存形式为对象,如果传入的对象有key属性,那么key会作为索引值保存,如果没有key属性,则自动生成一个key值,然后保存在table._index_中,上面的例子的操作结果如下图:

1

batch(array, callback)//保存一组对象

上面的例子改用batch方法就是:

1
2
3
4
5
6
7
8
9
var store = Lawnchair({name:'table'}, function(store) {
});
store.batch([{
key:'hust',
name:'xesam_1'
},{
key:'whu',
name:'xesam_2'
}])

exists (key, callback)//检查是否存在key,并将结果的布尔值(true/false)传递给callback函数

1
2
3
4
5
6
store.exists('whu',function(result){
console.log(result);//true
})
store.exists('test',function(result){
console.log(result);//false
})

get (key|array, callback)//获取一个或者一组对象,然后调用callback处理

1
2
3
store.get('hust',function(result){
console.log(result);//{key:'hust',name:'xesam_1'}
})

all (callback)//将所有对象放在一个数组返回

1
2
3
store.all(function(result){
console.log(result);//[{key:'hust',name:'xesam_1'},{key:'whu',name:'xesam_2'}]
})

each(callback)//遍历集合,将(对象,对象索引)传递给callback函数

1
2
3
4
5
store.each(function(result){
console.log(result);
//{key:'hust',name:'xesam_1'}
// {key:'whu',name:'xesam_2'}
})

remove (key|array, callback)//移除一个或者一组元素。

1
2
3
4
5
store.remove('whu',function(){
store.all(function(result){
console.log(result)//[{key:'hust',name:'xesam_1'}]
});
})

nuke (callback)//销毁所有

1
2
3
4
5
store.nuke(function(){
store.all(function(result){
console.log(result)//[]
});
})

keys (callback) //返回存储对象的所有keys

1
2
3
store.keys(function(result){
console.log(result)//['hust','whu']
})

lawnchair.js的核心很小,然后有完善的扩展和插件机制,可以按需加载。
自己编写也比较方便,只需要在自己的代码中实现

adapter 
valid 
init 
keys 
save 
batch 
get 
exists 
all 
remove 
nuke

方法即可。

【Javascript】Sizzle引擎--原理与实践(五)

这篇是关于Sizzle.filter部分的详细分解,前提是先看懂Sizzle.filter的分发过程。

回顾一下Sizzle.filter的过程:

1
2
3
4
match = preFilter()
if(match){
filter()
}

当preFilter返回为false的时候,filter就被短路了。

过滤CLASS

怎么判断一个元素时候含有某个类,源码中的方法:

1
2
3
function checkClass(elem,className,not){
return not ^ (' ' + elem.className + ' ').indexOf(' '+ className +' ') > -1;
}

注意那个not参数,前面有提到过的,取补集而已。

checkClass(elem,className)  --->hasClass(elem,className)
checkClass(elem,className,true)  --->hasNoClass(elem,className)

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="test" class="a b c d"></div>
<script type="text/javascript">
function checkClass(elem,className,not){
return not ^ (' ' + elem.className + ' ').indexOf(' '+ className +' ') > -1;
}
var test = document.getElementById('test');
console.log(checkClass(test,'a')); //1
console.log(checkClass(test,'b')); //1
console.log(checkClass(test,'e')); //0

var test = document.getElementById('test');
console.log(checkClass(test,'a',true)); //0
console.log(checkClass(test,'b',true)); //0
console.log(checkClass(test,'e',true)); //1
</script>

对照

1
Expr.match.CLASS = /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/;

获得的一个类似[‘.className’,’className’]的数组,取做match,经过层层处理之后,传递给preFilter.CLASS的形式主要是[‘.className’,’className’,?,?]的形式,因此在CLASS处理部分,只需要match的前两个参数就可以了。

因此在CLASS的preFilter部分,其大致处理形式就是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
preFilter.CLASS =  function( match, curLoop, inplace, result, not, isXML ) {
match = " " + match[1].replace( rBackslash, "" ) + " ";
if ( isXML ) {//是否是XML,咱可以不考虑
return match;
}
//依次遍历curLoop中的待检测元素
for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
if ( elem ) {
//找到对应的类
if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) {
if ( !inplace ) {//找到对应的类,而且不允许就地修改的时候,将满足条件的存入result
result.push( elem );
}
} else if ( inplace ) {//没有对应的类,而且允许就地修改的时候,将对应项的数据改为false
curLoop[i] = false;
}
}
}
return false;//这里在非XML的情况下,都是返回false,因此后面的filter部分始终被短路
};

在上面的步骤中,需要注意的是curLoop已经result是两个数组,属于引用类型,任何改变都会反映到外部的集合中。已经得到了过滤的结果,因此返回false,无需再执行其他步骤。

至于filter.CLASS就是单纯的判断是否含有某个类而已:

1
2
3
filter.CLASS: function( elem, match ) {
return (" " + (elem.className || elem.getAttribute("class")) + " ").indexOf( match ) > -1;
},

上面的代码已经很清晰了。

过滤ID

1
ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/

match的形式如[‘#id’,’id’]

这个步骤就很简单了,

preFilter处理match[1]的格式问题

1
2
3
preFilter.ID: function( match ) {
return match[1].replace( rBackslash, "" );
},

filter判断元素的id是不是给定的id

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
    filter.ID: function( elem, match ) {
return elem.nodeType === 1 && elem.getAttribute("id") === match;
},
```

一般来说,要判断id的情况并不多,因为id一般都在Sizzle.find被处理掉了,再来过滤id严重拖慢速度,expr表达式写得估计有问题。

### 过滤TAG

同过滤ID

### 过滤ATTR

过滤attr的过程在filter部分的时候作为实例带过,此处忽略

### 过滤Child

Child有点复杂了,可以从选择符的形式着手。

举个栗子:

```html
<div>
<p class="tab" id="a1">1</p>
<p class="tab" id="a2">2</p>
<p class="tab" id="a3">3</p>
</div>
<div>
<p class="tab" id="a4">4</p>
</div>
<script type="text/javascript">
console.log(Sizzle('p:only-child')); //[#a4]
console.log(Sizzle('p:last-child')); //[#a3,#a4]
console.log(Sizzle('p:first-child')); //[#a1,,#a4]
console.log(Sizzle('p:nth-child(1)')); //[#a1,#a4]
console.log(Sizzle('p:nth-child(odd)')); //[#a1,#a3,#a4]
console.log(Sizzle('p:nth-child(even)'));//[#a2]
console.log(Sizzle('p:nth-child(n)')); //[#a1,#a2,#a3,#a4]
console.log(Sizzle('p:nth-child(2n+1)'));//[#a1,#a3,#a4]
</script>

在上面的几个形式中,可以分为两组:

only-child,last-child,first-child分为一组A

nth-child单独为一组B。

虽然由 nth-child 可以完全得到组A,但是组A是特殊形式的简便方式。

另外,这两种方式的代码形式的主要是组A不能再带位置参数,组B必须后接一个位置参数。

比如‘last-child(2)’是不行的,‘nth-child’是不行的,这个看名字就可以理解。

先看

1
CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,

匹配(match)的分组就是match = [‘nth-child’,’nth’,’2n+1’]或者match = [‘first-child’,’first’,undefined]的形式。
其中nth那种的位置表达式可能比较多,因此preFilter中处理的问题就是把nth形式中的各种形式转换成统一的形式。

转换原理

even 转换成 2n,odd 转换成 2n+1,5 转换成 0n+5 ,可得最后要得到的形式就是 an+b 的形式。

1
2
3
4
5
match[2] === "even" && "2n";//对于even的形式转化为2n
match[2] === "odd" && "2n+1;"//对于odd的形式转化为2n+1
!/\D/.test( match[2] ) && "0n+" + match[2];//对于纯数字(比如'nth-child(5)')的形式转化为'nth-child(0n+5)'
match[2];//其他形式('3n+4')的直接保留
//这里理解&&这种操作符的用途就行,虽然某些人说这种用法并不提倡。

现在形式统一了,只需要提取出an+b中相应的a和b就可以了,提取方式当然用正则,注意a、b都可能为负数。

1
var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec('an+b');

获得的test分组类似[‘an+b’,’-‘,’a’,’+b’],对于实例‘-2n-1’就是[‘-2n-1’,’-‘,’2’,’-1’]

现在每个数组都是字符串,需要时数字才便于处理,因此我们先将数字字符串转换为纯数字,字符串转换为数字也挺简单:

1
number = numberStr-0

因此这个基本步骤就是:

1
2
3
4
var posStr = match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||!/\D/.test( match[2] ) && "0n+" + match[2] || match[2];
var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec(posStr);//分离an+b
var temp1 = (test[1] + (test[2] || 1)) - 0; //'a'转化为a,带符号
var temp2 = test[3] - 0; //‘b’转化为b,也带符号

看具体源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CHILD: function( match ) {
if ( match[1] === "nth" ) {
if ( !match[2] ) { //nth的形式必须有位置参数
Sizzle.error( match[0] );
}
match[2] = match[2].replace(/^\+|\s*/g, '');//去除开头的加号或者结尾的空白
//下面的步骤就对应于上面的分解步骤
var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec(
match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||!/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
match[2] = (test[1] + (test[2] || 1)) - 0;
match[3] = test[3] - 0;
//到这里的时候,match的形式已经统一成['nth-child(??)','nth',a,b]的形式
}
else if ( match[2] ) {//非nth的形式一定不能有位置参数
Sizzle.error( match[0] );
}

match[0] = done++;
//到这里的时候,match的形式已经统一成[done,'nth',a,b]的形式
return match;
},

过滤POS

过滤PSEUDO

【Javascript】Sizzle引擎--原生getElementsByClassName对选择结果的影响(jQuery)

个人觉得这个例子虽然可能不具有实际意义,但是可以很好的理解Sizzle选择的过程

实例说明

先看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
</head>
<body>
<p class="tab" id="a1">1</p>
<p class="tab" id="a2">2</p>
<p class="tab" id="a3">3</p>
<script type="text/javascript" src="../sizzle.js"></script>
<script type="text/javascript">
console.log(Sizzle('.tab:not(:first)'));
console.log(Sizzle('p:not(:first)'));
console.log(Sizzle('p.tab:not(:first)'));
</script>
</body>
</html>

看上面三个结果的三个表达式,估计很多人会觉得结果肯定是一样的,不错,除去IE6/7/8,结果应该都是一样的,结果(一):

1
2
3
console.log(Sizzle('.tab:not(:first)'));  //[#a2,#a3]
console.log(Sizzle('p:not(:first)')); //[#a2,#a3]
console.log(Sizzle('p.tab:not(:first)')); //[#a2,#a3]

但是在IE6/7/8下面,结果(二):

1
2
3
console.log(Sizzle('.tab:not(:first)'));  //[#a1,#a2,#a3]
console.log(Sizzle('p:not(:first)')); //[#a2,#a3]
console.log(Sizzle('p.tab:not(:first)')); //[#a2,#a3]

其实不仅是IE6/7/8,任何不支持 getElementsByClassName 方法的浏览器结果都是结果(二)这样。

结果分析

在结果(一)的过程中,

‘.tab:not(:first)’选择流程是:

  1. document.getElementsByClassName(‘.tab’)获得结果集[#a1,#a2,#a3];
  2. 过滤:not(:first),获得结果集[#a2,#a3];

‘p:not(:first)’选择流程是:

  1. document.getElementsByTagName(‘p’)获得结果集[#a1,#a2,#a3];
  2. 过滤:not(:first),获得结果集[#a2,#a3];

‘p.tab:not(:first)’选择流程是:

  1. document.getElementsByClassName(‘.tab’)获得结果集[#a1,#a2,#a3];
  2. 过滤p,获得结果集[#a1,#a2,#a3];
  3. 过滤:not(:first),获得结果集[#a2,#a3];

在结果(二)的过程中:

‘.tab:not(:first)’选择流程是:

  1. document.getElementsByTagName(‘*’)获得结果集[html,head,body,#a1,#a2,#a3,script,script]
  2. 过滤:not(:first),获得结果集[head,body,#a1,#a2,#a3,script,script]
  3. 过滤.tab,获得结果集[#a1,#a2,#a3]

‘p:not(:first)’选择流程是:

  1. document.getElementsByTagName(‘p’)获得结果集[#a1,#a2,#a3];
  2. 过滤:not(:first),获得结果集[#a2,#a3];

‘p.tab:not(:first)’选择流程是:

  1. document.getElementsByTagName(‘p’)获得结果集[#a1,#a2,#a3];
  2. 过滤:not(:first),获得结果集[#a2,#a3];
  3. 过滤.tab,获得结果集[#a2,#a3]

可以看到,在不含class的选择符中,两种情况过程是一样的,当含有class的时候,会优先去寻找含有class的元素,从而直接影响了后面过滤步骤的候选集。

原因分析

至于产生的原因,我们回到代码层面来解释:

第一个影响因素

先看Sizzle.find部分

1
2
3
for ( i = 0, len = Expr.order.length; i < len; i++ ) {}

Expr.order: [ "ID", "NAME", "TAG" ];

最初的查找的时候,匹配type的是有一个优先级的 ID–>NAME–>TAG ,对于支持 getElementsByClassName 的浏览器,Sizzle 源码进行了一个处理:

在1296行:

1
2
3
4
5
6
Expr.order.splice(1, 0, "CLASS");
Expr.find.CLASS = function( match, context, isXML ) {
if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
return context.getElementsByClassName(match[1]);
}
};

于是在支持getElementsByClassName的浏览器中,Sizzle.find的实现变成

1
2
3
for ( i = 0, len = Expr.order.length; i < len; i++ ) {}

Expr.order: [ "ID","CLASS" "NAME", "TAG" ];

而且CLASS的优先级仅次于ID。

所以.tab:not(:first)在过滤的第一步,不同浏览器的候选集就已经发生了差异。

第二个影响因素

再看Sizzle.filter部分

1
2
3
4
5
6
7
8
9
10
for ( type in Expr.filter ) {}
Expr.filter={
CLASS: function( match, curLoop, inplace, result, not, isXML ) {},
ID: function( match ) {},
TAG: function( match, curLoop ) {},
CHILD: function( match ) {},
ATTR: function( match, curLoop, inplace, result, not, isXML ) {},
PSEUDO: function( match, curLoop, inplace, result, not ) {}
POS: function( match ) {}
},

这里与 Sizzle.find 的一个显著不同是这里用的 for..in 循环,没有确定的顺序【优先级】,因此并不能保证先过滤什么类型。

所以.tab:not(:first)在先过滤了:not(:first),然后再过滤.tab,所以结果和想想的不一致。

第三个影响因素

这个涉及到filter的实现,对于含有not的表达式,候选集合会被筛选一遍。

.tab:not(:first)在not部分集合改变,直接去除了第一个元素。

.tab:first 就没有改变,因为first是在POS匹配中处理的。

解决方案

如果我们把Sizzle.filter的实现改成和Sizzle.find一致,变成如下形式:

1
2
3
for ( i = 0, len = Expr.filterOrder.length; i < len; i++ ) {}

Expr.filterOrder: [ "ID","CLASS" "NAME", "TAG" ,"ATTR","CHILD","PSEUDO","POS"];

那么我们就可以保证选择的结果和预期的一致。不过这种牵一发而动全身的事情,显然是不适合的。

另外一个方法就是避免使用这种有歧义的选择符,将:not(:first)作为集合的一个方法调用,而不直接写到选择表达式里面。

另外,如果知道DOM的结构,处理方式就很多了,比如加tag限制,层级限制等等

说明

这种问题并不是出现在没个类似的选择表达式中,具体的情况可以查看Sizzle源码的filter部分。

filter部分比较麻烦,有什么错误之处,欢迎交流。

【Javascript】Sizzle引擎--原理与实践(四)

元素过滤是Sizzle中最复杂的一部分

基本形式

1
Sizzle.filter = function( expr, set, inplace, not ) {}
expr    过滤表达式
set      候选集合
inplace 是否原地修改
not      是否取补集

说明:set传递的是一个集合(数组),如果inplace为true,set会被修改;not的作用是用来取补集。还是用个例子来说明下:

1
2
3
<li id="a1" class="test"></li>
<li id="a2" class="test"></li>
<li id="a3" ></li>

原始集合

set = [#a1,#a2,#a3]

(1)

filter('.test',set,true,false)
-->   result = [#a1,#a2]
-->   set    = [#a1,#a2]

(2)

filter('.test',set,false,false)
-->   result = [#a1,#a2]
-->   set    = [#a1,#a2,#a3]

(3)

filter('.test',set,true,true)
-->   result = [#a3]
-->   set    = [#a1,#a2]

(4)

filter('.test',set,false,true)
-->   result = [#a3]
-->   set    = [#a1,#a2,#a3]

基本流程

在前面的find中,我们获得了一个类似

1
2
3
4
{
expr : expr,
set : set
}

的结果。其中,expr是过滤表达式,set是候选集合

简要说一下Sizzle.filter的过程:

第一步:过程和find差不多,提取expr中的过滤类型,相比find的类型,filter的类型有7种,分别是:

ID选择器,CLASS选择器,TAG选择器,ATTR属性选择器,CHILD子元素选择器,PSEUDO伪类选择器,POS位置选择器
1
['ID','TAG','ATTR','CHILD','POS','PSEUDO']

由于filter的情况太过复杂,所以,在执行最终的过滤之前,都会先执行一步预过滤,来统一调用。对应到源码中就是Expr.preFilter和Expr.filter

第二步:预过滤

第三步:最终过滤,true表示结果符合条件,false表示不符合

第四步:去除expr中已经匹配的部分,重复上面的操作,知道expr全部完毕或者set为空

因此,Sizzle.filter的大致代码流程是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Sizzle.filter = function( expr, set, inplace, not ) {
while ( expr && set.length ) {
for ( type in Expr.filter ) {
if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { //第一步
match = Expr.preFilter[ type ]( ? ); //第二步
if ( match ) {
found = filter( ? );//第三步
}
}
if ( found !== undefined ) {//第四步
expr = expr.replace( Expr.match[ type ], "" );
break;
}
}
}
return curLoop;
}

实例说明

沿用find部分的例子:

1
2
3
<input type="radio" id="a" name="gender" value="man" class="default" /><label for="a" ></label>
<input type="radio" id="b" name="gender" value="man" class="default" /><label for="b"></label>
<input type="checkbox" id="c" name="gender" value="man" /><label for="c">人妖</label>

在第一步的find中,我们获得的返回值是

1
2
3
4
{
expr : '[class*="default"]',
set : Array.prototype.slice.call(document.getElementsByTagName('input'))
}

filter的情况很多,因此单独抠出例子中用到的部分:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
var Expr = {
match : {
ATTR : /(^(?:.|\r|\n)*?)\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\4|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\](?![^\[]*\])(?![^\(]*\))/
},
attrMap: { //见讨论部分
"class": "className",
"for": "htmlFor"
},
attrHandle: {//见讨论部分
href: function( elem ) {
return elem.getAttribute( "href" );
},
type: function( elem ) {
return elem.getAttribute( "type" );
}
},
preFilter : {//预过滤
ATTR : function(match){
//var name = match[1] = match[1].replace( rBackslash, "" );这里被我屏蔽了,暂不考虑有\的情况
var name = match[1];
if (Expr.attrMap[name] ) {
match[1] = Expr.attrMap[name];
}
//match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" );这里被我屏蔽了,暂不考虑有\的情况
if ( match[2] === "~=" ) {
match[4] = " " + match[4] + " ";
}
return match;
}
},
filter : {//过滤
ATTR : function(elem , match){
var name = match[1], //选择表达式的属性名
result = Expr.attrHandle[ name ] ? Expr.attrHandle[ name ] :
elem[ name ] != null ? elem[ name ] : elem.getAttribute( name );
var value = result + "", //元素的实际值
type = match[2], //选择表达式的符号类型
check = match[4]; //选择表达式的目标值

return result == null ? type === "!=" :
!type && Sizzle.attr ? result != null :
type === "=" ?value === check :
type === "*=" ?value.indexOf(check) >= 0 :
type === "~=" ?(" " + value + " ").indexOf(check) >= 0 :
!check ? value && result !== false : //待检测的属性值为空
type === "!=" ? value !== check :
type === "^=" ?value.indexOf(check) === 0 :
type === "$=" ?value.substr(value.length - check.length) === check : //indexOf(value.length - check.length) >= 0
type === "|=" ?value === check || value.substr(0, check.length + 1) === check + "-" :
false;
}
}
};
var expr = '[class*="default"]';
var curLoop =Array.prototype.slice.call(document.getElementsByTagName('input'));

function filter(expr, set){
var match,item,i,found,left;
var curLoop = set,result=[];
while(expr && set.length){
if((match = Expr.match.ATTR.exec(expr)) != null){
left = match[1];
match.splice(1,1);
//预过滤,主要是处理形式和兼容性的问题
match = Expr.preFilter[ 'ATTR' ]( match); //注意这里与jquery的区别,多余的参数都去掉了

if(match){
for ( i = 0; (item = curLoop[i]) != null; i++ ) {
if ( item ) {
found = Expr.filter['ATTR']( item, match, i, curLoop );//true代表满足条件
if(found){
result.push(item);
}
}
}
if ( found !== undefined ) {
expr = expr.replace( Expr.match[ 'ATTR' ], "" );
}
}
}
}
return curLoop;
}
var s = filter(expr, curLoop);
console.log(s); //[#a,#b]

对照上面的代码:

第一步、Expr.match.ATTR被匹配

第二步、预过滤,在这一步中,需要处理的问题是属性选择器的“属性值”,“属性操作符”,“属性值”的逻辑与兼容性问题,ATTR中return的结果可能不好理解,一行一行的看比较好。

第三步、通过Expr.filter.ATTR依次检测set中每一项,符合过滤条件的就保存,不符合的就丢弃。

第四步、选择表达式修剪,开始下一轮。

具体到源码中的实现,流程是一样的,只是多了两个参数,注意一下not就行,并没有显示的传递not为true或者false,而是经过了运算或者隐式转换:

undefined --> false

true^undefined -->1(true)

^异或操作符平时用的不多,这里可以复习下,源码中的逻辑就是:

1
2
3
4
5
if(not==true){
pass = !found
}else{
pass = found
}

因此设置not=true可以获得与默认情况下的原始集合互补的集合,就像文章开头一样。

讨论

1、a标签的 href 属性

关于这个的讨论网上有不少,可以参考:

  1. http://www.planabc.net/2008/11/06/ie-href-bug/
  2. http://www.quirksmode.org/bugreports/archives/2005/02/getAttributeHREF_is_always_absolute.html
  3. http://www.glennjones.net/2006/02/getattribute-href-bug/

源码中对于这个的处理是单独定义了Expr的一个属性:

1
2
3
4
5
6
7
8
attrHandle: {
href: function( elem ) {
return elem.getAttribute( "href" );
},
type: function( elem ) {
return elem.getAttribute( "type" );
}
},

然后在源码的1130行,

1
2
3
4
5
6
7
8
div.innerHTML = "<a href='#'></a>";

if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
div.firstChild.getAttribute("href") !== "#" ) {
Expr.attrHandle.href = function( elem ) {
return elem.getAttribute( "href", 2 );
};
}

2、关于新增的type

上面的type在以前的源码中是没有的,这个是新加的,原因就是HTML5新增了许多type类型,但是老式浏览器直接获取elem.type并不能获取新的类型,所以采用getAttribute方法。

3、两个特殊属性class和for。

这两个应该都清楚,class是保留字,for是,都不能用属性操作符来获取属性,所以单独拎出来处理。

整个Sizzle.filter也是一个分发器的作用,相比find的三种情况,filter的情况就复杂多了,后面再一个个的分析实现,以及这么做的思想。