‘JavScript应用思想录’ 所有文章

简化动态设置CSS:建立Inline+CSS Style Parser

一月 22nd, 2010 由 Rock 发表

有时动态设置元素的inline style也是很有必要的,因为它有一些CSS没有的优点:

  • inline style不会产生子属性继承
  • 利用inline style优点级,改变CSS属性(important除外)

该Parser的一些特点:

1. 以元素+规则字符串作为输入,以改变元素style样式作为输出

2. 定义一系列规则,由规则改变当前Parser状态

3. 规则可以嵌套

4. 规则可自定义

由以上功能可看出,只要定义了一些元规则,就可以产生一个强大的规则解析器.

结合CSS的一些特性,现在将这些规则具体化

rule -> rule S rule
rule -> prefix text
S -> /\s+/
prefix -> -        //应用样式到首层子元素
prefix -> $      //样式只对Border Box的浏览器有效
prefix ->.        //标记样式作为css类加入
text -> name|name=value        //名称,或配有值的名称
name -> /\S+/           //规则名称
value -> /\S+/          //最终应用值

现在把name=value作为一个对象的属性键值存储,就形成一个规则的集合
当text -> name|name=value,只有name而没值时就从这集合中取出值来.

好了,描述有了,举个例子

var ruleSet = {
“1c” : ["width","100%"],
“2c” : ["width","50%"],
“3c” : ["width","30%"],
“np” : ["padding","0px"],
“bdr” : ["border","0px"],
‘fl’ : ["float", 'left'],
‘cls’:['clear', 'both'],
‘cascade’:'1c np fl cls’
};

如 ‘bdr=3 cascade’对应的的CSS样式为

{
width:50%;
padding:0px;
border :3px;
float:left;
clear:both;
}

例如只针对非Border Box模型有效的浏览器应用
‘$bdr=2′

例如针对非Border Box模型有效的浏览器元素所有首层子元素应用
‘-$bdr=2′

例如给元素添加css类
‘-$bdr=2 .class_a .class_b’

扩展方面,可对上面的一些方法描述进行扩展,使得规则定义得更加强大

在prefix层,可定义多样过滤符号

在value层,可定义value类型不仅仅是”值”,还可以是一个函数,真正值由函数实时计算后返回;value也还可以是一个JS对象,批量设置element.style的属性对等.

这个Style Parser功能强大,实现起来也简单,不失为把问题简单化处理的一个好方法.

可以把这Style Parser挂到jQuery对象或其它元素封装过的对象上,最终形式类似:

element.parse(’bdr=3 $cascade’);

这一句就可以应用到下面的样式:

{
border :3px;
/**以下是border box模型适用*/
width:50%;
padding:0px;
float:left;
clear:both;
}

该CSS Parser已应用到Cicy JS库中,对于表单的设计挺方便.

IE下遇到奇怪的问题:左击event.button为0!

一月 15th, 2010 由 Rock 发表

判断鼠标左击方法是:
isLeftClick: function(ev) {
return (((ev.which)
&& (ev.which === 1)) || ((ev.button) && (ev.button === 1)));
}

但今天对话框在IE下左击的时候,event.button却是0,

isLeftClick返回了false,ie6-ie8均如此,

用IE8调试了一下

event.type = ‘click’;没错,,
event.button = 0;
据我所了解,为0即表示无按键,我左击了难道无形中消失了?
我并没有设置setTimeout作延迟,按理还在click事件处理的回调中的,也就是window.event未被重设.

不要再折腾JavaSciprt 实现 OOP

十二月 19th, 2009 由 Rock 发表

OOP三个代表:

1. 封装

JavaScript支持

对象数据结构: { attributeName : attributeValue }

对象属性公开访问

object.attributeName

目前JavaScript在属性访问封装上的缺陷:缺少自定属性getter与setter规则

object.attributeName.__defineGetter__ = function(){ return ..}
object.attributeName.__defineSetter__ = function(){ this.attributeName = ..}

成员反射

for (var key in object) …

2. 继承与接口

JavaScript支持

原型链实现继承

类表示:

function Class(){}

类实例化

var clazz = new Class();

常见的Object类实例:
var obj = {};

继承实现

父类
function Father(){}

//用于连接父子类原型链,现称为桥接类
function Bridge() {}

子类
function Sun(){}

//建立原型链
Bridge.prototype = Father.prototype;

Sun.prototype = new Bridge();

//正确指定constructor
Sun.prototype.constructor = Sun;

桥接类好处在于,修改子类原型并不会影响到父类原型

完成父子类关系构建后,以后可给父子类作扩展

Father.prototype.say = function(){}

var son = new Son();

son.say();

Sun.prototype.sayOk = function(){}

son.sayOk();

实现接口

利用JavaScript的call或apply方法

classA.method.apply(classB, arguments);

3. 多态

JavaScript本身支持的方法传递可变参数与泛型

function action(arg0, arg1, …, argN);

从特征上检测对象数据类型

十月 14th, 2009 由 Rock 发表

本文关键字

  • 基本数据类型
  • 对象类型
  • 装箱操作
  • 运算符重载

最近也少写东西了,今天看到网友 topcss 的评论

http://www.bgscript.com/archives/24#comment-159

有感,在分析我为什么要那样写之余,谈谈灵活的JavaScript语言系统中对”对象”类型的检测法.

JavaScript中的运算符重载:valueOf()

有些人可能会觉得很奇怪,JavaScript中没像C++语言中提供运算符重载这强大的功能,我想说的是,灵活的JavaScript也可做到运算符重载,虽然能力有限.

Object对象有个方法可能大家平时很少关注,其实这方法正是贯穿本文的方法,那就是valueOf(),且看例子,JS中怎么实现运算符重载.

obj.valueOf() : 返回指定对象的原始值,即对象求值,JavaScript中利用该值参与对象的运算操作.

  alert(+ new Date());
  alert( (new Date()).valueOf());
  alert(new Date() + new Date());

在上面的语句中,JS引擎正是调用Date对象的valueOf求得值并参与算术运算符+的运算.

得知过程后,众所周知,两字符串间的+运算是字符串的连接操作,

   alert(new String('string1') + new String('string2'));

假如我要重载字符串对象的运算符,使得字符串相加时是字符串长度相加而非连接,怎么做?可以这样,重写valueOf方法:

String.prototype.valueOf = function(){
   return this.length;
}
 
//显示5
alert(new String('ss') + new String('www'));

有些同学可能觉得很奇怪了,为什么

  alert('ss' + 'www');

还是显示 ’sswww’,

其实这也是正常的,因为’ss’,'www’这些是JS的基本类型,未经过装箱操作使得它们变成字符串对象实例,所以在+号运算时JS引擎还是执行默认的字符串连接操作.只要改改,使得基本数据类型在运算时JS引擎对其进行装箱操作后就可以了.

  String.prototype.boxUp = function(){
     //这里不对对像进行其它操作,就返回自身,即证明基本类型已被装箱
     return this;
  };
  // 下面这句就能正常如愿了,显示 5 
  alert('ss'.boxUp() + 'www'.boxUp());
  //显示 6
  alert('ss'.boxUp() * 'www'.boxUp());

好了,经过上面的讨论,有了结论:

JS中对象的所具有的行为与运算方式是通过方法来定义

回到话题,利用上面的方法我们可以构建一个”数字类”,且不讨论这”数字类”是否为一”数字”类.

我们参照Number类的特性,类可以进行算术运算符,逻辑运算符与字符串转换操作.

先定义好这类,名为 NumberB,与Number类一样,接受一个基本数字类型

  function NumberB( num ){
     this.value = Number(num);
     return this.value;
  }

再定义一个类,作为NumberB的”父类”,起桥接作用,暂且不管这桥接的作用,下文会说到.

 function Bridge(){};

NumberB原型指向Bridge实例

  NumberB.prototype = new Bridge();

之后再重载valueOf方法,使得NumberB类能参与运算符运算操作,与Number行为一致.

NumberB.prototype.valueOf = function(){
   return this.value;
};

再重载toString,进行正确的字符串转换.

NumberB.protoype.toString(){
  return this.value + '';
};

现在,该类的行为与Number基本一致了,也可进行运算,可以测试一下:

  var a = new NumberB(10),
       b = new NumberB(5);
 
  alert(a + b);
  alert(a / b);
  alert(a + b + ', hello');

好了,到目前为止,你可能还不大信息这类是一个”数字类”,基于JavaScript的灵活特性,下面将使得它变得更像点,那就是通过原型使得该类”继承”自Number类, 符合instanceof 原型链检测.

这里就用到上面的桥接类了,将上面的的桥椄类代码改进为:
注意下面的第四句,第四句类NumberB的原型被new Bridge()覆盖后,其原型的构造器属性已被覆盖,
在覆盖前为 alert(NumberB.prototype.constructor === NumberB);
最后一句将其修正.

1
2
3
4
5
6
7
  function Bridge(){};
  // 该句使得类"继承"自Number
  Bridge.prototype = Number.prototype;
  NumberB.prototype = new Bridge();
 
  //修正NumberB类的constructor属性,使其指向正确的NumberB
  NumberB.prototype.constructor = NumberB;

上面将类NumberB”继承”自Number类后,符合了instanceof检测,

  //显示 true
  alert( new NumberB('5') instanceof Number);
  //显示 NumberB
  alert( new NumberB('5').constructor);

好了,呵呵,现在你还怀疑NumberB不是一个”数字类”么.

再回到讨论的话题, 判断一个对象是否为”数字”,两个方法比较一下,将从两方面说说我为什么不采用obj.constructor来检测.

下面两个方法经笔者优化过,将==换成===.

 //利用constructor检测
 function isNumber(ob) {
   return ob.constructor === Number;
}

//利用tyoeof 与 instanceof检测
function isNumberB(ob) {
   return typeof ob === "number" || ob instanceof Number;
}

1. 对对象混合类型的判断方面

先弄清楚什么是数字类型?

笔者觉得作为数字类型的对象至少要满足其基本数据类型的行为,如对基本数据类型的运算操作等.
从这意义上去,在类型上面的NumberB与Number类是等价的,Number类是内置的对象,作为对基本数据类型的一个对象化封装,而NumberB是对其的另一个对象封装,不同的是,Number对象是JavaScript引擎默认的基本数据类型装箱对象,所以就往往采用ob.constructor === Number去检测,上面的NumberB例子已说明,

假如按特征来判断一个对象是否为数字类型的话, obj.constructor === Number是obj为数字类型的充分非必要条件.

如上面的NumberB类,有些对象通过原型的继承关系获得父类的大部份行为,
在上面NumberB例子中,按数字类型的特征判断,NumberB类可看作是数字类,isNumber方法不能检测,因为构造器却不是Number,此时,isNumberB方法能测出该对象是数字类.

  var num = new NumberB(5);
  //false
  alert( isNumber(num) );
  // true
  alert( isNumberB(num) );

2. 性能方面

下面具体分析这两个方法的性能.

传入的参数数据类型对性能有很关键影响.

参数有两种类型:

  • 1. 基本数据类型, 如 func( 5 );
  • 2. 对象类型 , 如 func(new Number(5));

对于函数:

function isNumber(ob) {
return ob.constructor === Number;
}

如果传进的是基本数据类型,
所要进行的操作有:

  • 对基本数据类型的装箱操作
  • 装箱成Number对象实例后,寻找原型链上的constructor属性
  • 对象引用比较

如果传进的是对象类型

所要进行的操作有:

  • 寻找原型链上的constructor属性
  • 对象引用比较

所以对对象的操作少了装箱操作.

但日常我们用到的数字多是基本的数据类型,所以大多数情况下isNumber方法少不了装箱操作.

对于函数:

function isNumberB(ob) {
return typeof ob === “number” || ob instanceof Number;
}

如果传进的是基本数据类型,
所要进行的操作有:

  • typeof ob 检测无需装箱
  • 值比较

如果传进的是对象类型

所要进行的操作有:

  • 如果为Number实例,引用比较返回true
  • 如果非Number实例,如上面的NumberB类实例,将进行 instanceof 比较原型链

可见,对于对象类型而非Number的实例对象,isNumberB将进行更多的比较.

但日常我们用到的数字多是基本的数据类型,所以大多数情况下isNumberB方法将执行typeof检测后返回正确的.

可见,如果传进的是基本数据类型的话,无论如何,isNumberB都不会比isNumber慢,如果是对象类型或非数字类型的对象isNumberB有相当可能比isNumber慢.

下面是笔者在firefox,ie,chrome 下的测试数据,判断运行一百万次:

var n = new Number(5);
//   n = new NumberB(5);
var d = (new Date()).valueOf();

for(var i=0;i<1000000;i++){
	 isNumber(n);
         // isNumberB(n);
}
 alert(new Date() - d);

// 结果
 //firefox 3.5.3
   isNumberB(n) 407 ms
   isNumberB(5) 7 ms
 //--
   isNumber(n) 610 ms
   isNumber(5) 1555 ms

 // ie 8
   isNumberB(n) 1110 ms
   isNumberB(5) 813 ms
 //--
   isNumber(n) 970 ms
   isNumber(5) 954 ms

 //chrome
   isNumberB(n) 51 ms
   isNumberB(5) 38 ms
 //--
   isNumber(n) 53 ms
   isNumber(5) 170 ms

额外话:

IE对基本数据类型的typeof检测,真是令我对IE的JS引擎”佩服”得五体投地,
一般JS引擎的基本数据类型数据结构如下:

  struct Value {
     //类型标记
     int dataType;
     Data *data;
     ...
  }

对于typeof 检测,直接判断dataType取值即可,所以理应非常快的.这方面firefox做得最好,居然7ms就能完成任务.
但IE耗时居然和对象类型差不多,可能并没优化,一律进行装箱操作 -_-!!!

如何有效的捕获JavaScript焦点

八月 14th, 2009 由 Rock 发表

阅读本文可理解并解决以下问题:

  • 设置元素可获得焦点以监听键盘事件
  • 某个元素明明设置了聚焦却没效果
  • 聚焦时抛出异常的

1. 设置元素可获得焦点以监听键盘事件

元素聚焦最大好处就是可允许发送键盘事件,HTML很多元素默认就有可聚焦,如form表单元素,a锚链接等,但大部份默认是不能聚焦的。要使得元素能够聚焦,可以在HTML代码或JavaScript脚本中实现。

html:

  <div tabIndex="0" style="height:100px;width:100px; background:red;"></div>

JavaScript:
oDiv.tabIndex = 0;

其中tabIndex是TAB键的导航顺序,可有正,负或零。

当元素获得焦点时会有边框指示,如果想不显示这个边框,可以

html:

<div tabIndex="0"  hidefocus="on" ></div>

JavaScript:
oDiv.hideFocus = ‘on’;

2. 元素明明设置了聚焦却没效果

有时用JavaScript设置了元素聚焦,但最后焦点却不落在该元素上,百思不得其解。

问题在于如果在可焦点元素的事件处理函数中聚焦其它元素,就有可能聚不了焦点,因为如果该事件是个可获得焦点的事件,如mouse, keydow(keypress)等等,在这些事件的处理函数内直接聚焦其它元素是失败的。

oDiv.onmousedown = function(){
   document.getElementById('ipt').focus();
};

参考浏览器内核处理流程图:

当浏览器第一次Reflow回流后,焦点停在另一个元素上,但回流返回后,事件处理后默认的操作将继续执行,那就是聚焦到事件源,也就是mousedown的元素,这时引发第二次回流,当回流后焦点聚在该元素上.所以在事件处理函数中的聚焦变得无效.

有没解决方法? 答案是肯定的. 由图可知,只要把聚焦放到第二个Reflow回流之后执行即可.这个可利用setTimeout方法作延迟先放进队列等后再执行.因为由于JavaScript引擎单线程特性,图上整个过程都是连着执行的,该过程中JS引擎一直没有空闲过,当上面所有操作都完成后并后,定时回调才有机会被执行.所以可以:

oDiv.onmousedown = function(){
  setTimeout(function(){
      document.getElementById('ipt').focus();
   }, 0);
};

由上可知,最后那个毫秒数即使设为0也没关系.

3. 聚焦时抛出异常的
在IE中,当元素不可见时如果聚焦的话,会抛出一个异常,因为在很多应用中我们往往不再对元素是否不可见作测试就聚焦了,因为即使这样也没什么问题(谁说不可见元素就不可以聚焦的?)..所以,在IE下可用try{}catch(){}来忽略这个异常.

   try{
     element.focus();
   }catch(e){}

到此,与JavaScript焦点捕获相关的问题讨论就完成了.点击运行示例.

注意 JavaScript ‘delete 属性’ 的使用

八月 11th, 2009 由 Rock 发表

JavaScript对象数据结构基本形式:{ key : value},其中key:value就为对象的一个属性,key作为属性名称,value为属性值,这值可以是任何JavaScript数据类型。
delete 是删除对象的一个属性,例如对于一个对象,

var obj = {key:5};

delete obj.key就是删除该对象的key属性,这个没什么问题,但当对象的原型prototype对象也存在该属性时,就值得注意了。

 var A = function(){};
 A.prototype.testMe = true;
 var a = new A();
 //覆盖原型属性
 a.testMe = true;
 if(a.testMe){
  // 一些关键代码... 
  // ....
  //删除这属性
  delete a.testMe;
}
//第二段 ---------------------------
// 在其它模块中
 if(a.testMe){
  // 一些关键代码... 
  // ....
}

第二段是值得注意的,不要以为a中testMe已尼删除了就不存在了,所以a.testMe就为undefined,即为假,其实它通过原型访问还是存在的,还是true!
这里不留神就中招了。

//附:
检测对象是否存在某属性, 包括原型链的:
if (’attrName’ in obj)…
检测对象是否存在某属性,是对象本身的,而非原型链的:
obj.hasOwnProperty(’attrName’)

如何防止动态加载JavaScript引起的内存泄漏问题

七月 30th, 2009 由 Rock 发表

利用Script标签可以跨域加载并运行一段JavaScript脚本, 但Neil Fraser先前已指出,脚本运行后资源并没被释放,即使是Script标签移除后。为了释放脚本资源,通常在返回后还要一些进行额外的处理。

  script = document.createElement('script');
  script.src = 
     'http://example.com/cgi-bin/jsonp?q=What+is+the+meaning+of+life%3F';
  script.id = 'JSONP';
  script.type = 'text/javascript';
  script.charset = 'utf-8';
  // 标签加到head后,会自动加载并运行。
  var head = document.getElementsByTagName('head')[0];
  head.appendChild(script)

实际上很多流行的JS库都采用这种方式,创建一个scritp标签,赋予一个ID后加载脚本(比如YUI get()),加载完并回调后清除该标签。问题在于当你清除这些script标签的时候,浏览器仅仅是移除该标签结点。

var script = document.getElementById('JSONP');
script.parentNode.removeChild(script);

当浏览器移除这标签结点后的同时并没对结点内JavaScript资源的进行垃圾回收,这意味着移除标签结点还不够,还得手动的清除script标签结点的内容:

// Remove any old script tags.
  var script;
  while (script = document.getElementById('JSONP')) {
    script.parentNode.removeChild(script);
    // 浏览器不会回收这些属性所指向的对象.
    //手动删除它以免内存泄漏.
    for (var prop in script) {
      delete script[prop];
    }
  }

译自 : www.ajaxian.com

DOM标准与IE的html元素事件模型区别

七月 30th, 2009 由 Rock 发表

事件

HTML元素事件是浏览器内在自动产生的,当有事件发生时html元素会向外界(这里主要指元素事件的订阅者)发出各种事件,如click,onmouseover,onmouseout等等。

DOM事件流

DOM(文档对象模型)结构是一个树型结构,当一个HTML元素产生一个事件时,该事件会在元素结点与根结点之间的路径传播,路径所经过的结点都会收到该事件,这个传播过程可称为DOM事件流。

主流浏览器的事件模型

早在2004前在HTML元素事件的订阅,发送,传播,处理模型上各浏览器实现并不一致,直到DOM Level3中规定后,多数主流浏览器才陆陆续续支持DOM标准的事件处理模型 — 捕获型与冒泡型。
目前除IE浏览器外,其它主流的Firefox, Opera, Safari都支持标准的DOM事件处理模型。IE仍然使用自己的模型,即冒泡型,它模型的一部份被DOM采用,这点对于开发者来说也是有好处的,只使用DOM标准,IE都共有的事件处理方式才能有效的跨浏览器。

冒泡型事件(Bubbling)

这是IE浏览器对事件模型的实现,也是最容易理解的,至少笔者觉得比较符合实际的。冒泡,顾名思义,事件像个水中的气泡一样一直往上冒,直到顶端。从DOM树型结构上理解,就是事件由叶子结点沿祖先结点一直向上传递直到根结点;从浏览器界面视图HTML元素排列层次上理解就是事件由具有从属关系的最确定的目标元素一直传递到最不确定的目标元素.

捕获型事件(Capturing)

Netscape Navigator的实现,它与冒泡型刚好相反,由DOM树最顶层元素一直到最精确的元素,这个事件模型对于开发者来说(至少是我..)有点费解,因为直观上的理解应该如同冒泡型,事件传递应该由最确定的元素,即事件产生元素开始。
但这个模型在某些情况下也是很有用的,接下来会讲解到。

DOM标准事件模型

因为两个不同的模型都有其优点和解释,DOM标准支持捕获型与冒泡型,可以说是它们两者的结合体。它可以在一个DOM元素上绑定多个事件处理器,并且在处理函数内部,this关键字仍然指向被绑定的DOM元素,另外处理函数参数列表的第一个位置传递事件event对象。

首先是捕获式传递事件,接着是冒泡式传递,所以,如果一个处理函数既注册了捕获型事件的监听,又注册冒泡型事件监听,那么在DOM事件模型中它就会被调用两次。

注册与移除事件监听器

注册事件监听器,或又称订阅事件,当元素事件发生时浏览器回调该监听函数执行事件处理。目前主流浏览器中有两种注册事件的方法,一种是IE浏览器的,另一种是DOM标准的。

1.直接JS或HTML挂载法

<div onclick="alert(this.innerHTML);"></div>
  element.onclick = function(){alert(this.innerHTML);}

移除时将事件属性设为nul即可,这个也是最常用的方法了,优缺点也是显然的:

  • 简单方便,在HTML中直接书写处理函数的代码块,在JS中给元素对应事件属性赋值即可
  • IE与DOM标准都支持的一种方法,它在IE与DOM标准中都是在事件冒泡过程中被调用的。
  • 可以在处理函数块内直接用this引用注册事件的元素
  • 要给元素注册多个监听器,就不能用这方法了

2. IE下注册多个事件监听器与移除监听器方法

IE浏览器中HTML元素有个attachEvent方法允许外界注册该元素多个事件监听器,例如

  element.attachEvent('onclick', observer);

attachEvent接受两个参数。第一个参数是事件名称,第二个参数observer是回调处理函数。这里得说明一下,有个经常会出错的地方,IE下利用attachEvent注册的处理函数调用时this指向不再是先前注册事件的元素,这时的this为window对象了,笔者很奇怪IE为什么要这么做,完全看不出好处所在。
要移除先前注册的事件的监听器,调用element的detachEvent方法即可,参数相同。

    element.detachEvent('onclick', observer);

3. DOM标准下注册多个事件监听器与移除监听器方法

实现DOM标准的浏览器与IE浏览器中注册元素事件监听器方式有所不同,它通过元素的addEventListener方法注册,该方法既支持注册冒泡型事件处理,又支持捕获型事件处理。

  element.addEventListener('click', observer, useCapture);

addEventListener方法接受三个参数。第一个参数是事件名称,值得注意的是,这里事件名称与IE的不同,事件名称是没’on’开头的;第二个参数observer是回调处理函数;第三个参数注明该处理回调函数是在事件传递过程中的捕获阶段被调用还是冒泡阶段被调用

移除已注册的事件监听器调用element的removeEventListener即可,参数不变.

  element.removeEventListener('click', observer, useCapture);

跨浏览器的注册与移除元素事件监听器方案

弄清楚DOM标准与IE的注册元素事件监听器之间的异同后,就可以实现一个跨浏览器的注册与移除元素事件监听器方案:

  //注册
  function addEventHandler(element, evtName, callback, useCapture) {
     //DOM标准
      if (element.addEventListener) {
            element.addEventListener(evtName, callback, useCapture);
      } else {
         //IE方式,忽略useCapture参数
         element.attachEvent('on' + evtName, callback);
      }
  }
 
  //移除
  //注册
  function removeEventHandler(element, evtName, callback, useCapture) {
     //DOM标准
      if (element.removeEventListener) {
            element.removeEventListener(evtName, callback, useCapture);
      } else {
         //IE方式,忽略useCapture参数
         element.dettachEvent('on' + evtName, callback);
      }
  }

如何取消浏览器事件的传递与事件传递后浏览器的默认处理

先说明取消事件传递与浏览器事件传递后的默认处理是两个不同的概念,可能很多同学朋友分不清,或者根本不存在这两个概念。

取消事件传递是指,停止捕获型事件或冒泡型事件的进一步传递。例如上图中的冒泡型事件传递中,在body处理停止事件传递后,位于上层的document的事件监听器就不再收到通知,不再被处理。

事件传递后的默认处理是指,通常浏览器在事件传递并处理完后会执行与该事件关联的默认动作(如果存在这样的动作)。例如,如果表单中input type 属性是 “submit”,点击后在事件传播完浏览器就就自动提交表单。又例如,input 元素的 keydown 事件发生并处理后,浏览器默认会将用户键入的字符自动追加到 input 元素的值中。

要取消浏览器的件传递,IE与DOM标准又有所不同。

在IE下,通过设置event对象的cancelBubble为true即可。

  function someHandle() {
     window.event.cancelBubble = true;
  }

DOM标准通过调用event对象的stopPropagation()方法即可。

  function someHandle(event) {
     event.stopPropagation();
  }

因些,跨浏览器的停止事件传递的方法是:

  function someHandle(event) {
    event = event || window.event;
    if(event.stopPropagation)
       event.stopPropagation();
    else event.cancelBubble = true;
  }

取消事件传递后的默认处理,IE与DOM标准又不所不同。

在IE下,通过设置event对象的returnValue为false即可。

  function someHandle() {
     window.event.returnValue = false;
  }

DOM标准通过调用event对象的preventDefault()方法即可。

  function someHandle(event) {
     event.preventDefault();
  }

因些,跨浏览器的取消事件传递后的默认处理方法是:

  function someHandle(event) {
    event = event || window.event;
    if(event.preventDefault)
       event.preventDefault();
    else event.returnValue = false;
  }

捕获型事件模型与冒泡型事件模型的应用场合

标准事件模型为我们提供了两种方案,可能很多朋友分不清这两种不同模型有啥好处,为什么不只采取一种模型。
这里抛开IE浏览器讨论(IE只有一种,没法选择)什么情况下适合哪种事件模型。

1. 捕获型应用场合

捕获型事件传递由最不精确的祖先元素一直到最精确的事件源元素,传递方式与操作系统中的全局快捷键与应用程序快捷键相似。当一个系统组合键发生时,如果注册了系统全局快捷键监听器,该事件就先被操作系统层捕获,全局监听器就先于应用程序快捷键监听器得到通知,也就是全局的先获得控制权,它有权阻止事件的进一步传递。所以捕获型事件模型适用于作全局范围内的监听,这里的全局是相对的全局,相对于某个顶层结点与该结点所有子孙结点形成的集合范围。

例如你想作全局的点击事件监听,相对于document结点与document下所有的子结点,在某个条件下要求所有的子结点点击无效,这种情况下冒泡模型就解决不了了,而捕获型却非常适合,可以在最顶层结点添加捕获型事件监听器,伪码如下:

  function globalClickListener(event) {
     if(canEventPass == false) {
         //取消事件进一步向子结点传递和冒泡传递
         event.stopPropagation();
         //取消浏览器事件后的默认执行
        event.preventDefault();
     }
 }

这样一来,当canEventPass条件为假时,document下所有的子结点click注册事件都不会被浏览器处理。

2. 冒泡型的应用场合

可以说我们平时用的都是冒泡事件模型,因为IE只支持这模型。这里还是说说,在恰当利用该模型可以提高脚本性能。在元素一些频繁触发的事件中,如onmousemove, onmouseover,onmouseout,如果明确事件处理后没必要进一步传递,那么就可以大胆的取消它。此外,对于子结点事件监听器的处理会对父层监听器处理造成负面影响的,也应该在子结点监听器中禁止事件进一步向上传递以消除影响。

综合案例分析

最后结合下面HTML代码作分析:

<div id="div0" onclick="alert('current is '+this.id)">
<div id="div1" onclick="alert('current is '+this.id)">
<div id="div2" onclick="alert('current is '+this.id)">
<div id="event_source" style="height: 200px; width: 200px; background-color: red;" onclick="alert('current is '+this.id)"></div>
</div>
</div>
</div>

HTML运行后点击红色区域,这是最里层的DIV,根据上面说明,无论是DOM标准还是IE,直接写在html里的监听处理函数是事件冒泡传递时调用的,由最里层一直往上传递,所以会先后出现
current is event_source
current is div2
current is div1
current is div0
current is body

添加以下片段:

  var div2 = document.getElementById('div2');
  addEventHandler(div2, 'click', function(event){
     event = event || window.event;
     if(event.stopPropagation)
       event.stopPropagation();
     else event.cancelBubble = true;
  }, false);

当点击红色区域后,根据上面说明,在泡冒泡处理期间,事件传递到div2后被停止传递了,所以div2上层的元素收不到通知,所以会先后出现:
current is event_source
current is div2

在支持DOM标准的浏览器中,添加以下代码:

   document.body.addEventListener('click', function(event){
       event.stopPropagation();
  }, true);

以上代码中的监听函数由于是捕获型传递时被调用的,所以点击红色区域后,虽然事件源是ID为event_source的元素,但捕获型选传递,从最顶层开始,body结点监听函数先被调用,并且取消了事件进一步向下传递,所以只会出现
current is body

JavaScript可否实现多线程 —- 深入理解JavaScript定时机制

七月 16th, 2009 由 Rock 发表

容易欺骗别人感情的JavaScript定时器

JavaScript的setTimeout与setInterval是两个很容易欺骗别人感情的方法,因为我们开始常常以为调用了就会按既定的方式执行, 我想不少人都深有同感, 例如

setTimeout( function(){ alert(’你好!’); } , 0);
setInterval( callbackFunction , 100);

认为setTimeout中的问候方法会立即被执行,因为这并不是凭空而说,而是JavaScript API文档明确定义第二个参数意义为隔多少毫秒后,回调方法就会被执行. 这里设成0毫秒,理所当然就立即被执行了.
同理对setInterval的callbackFunction方法每间隔100毫秒就立即被执行深信不疑!

但随着JavaScript应用开发经验不断的增加和丰富,有一天你发现了一段怪异的代码而百思不得其解:

div.onclick = function(){
setTimeout( function(){document.getElementById(’inputField’).focus();}, 0);
};

既然是0毫秒后执行,那么还用setTimeout干什么, 此刻, 坚定的信念已开始动摇.

直到最后某一天 , 你不小心写了一段糟糕的代码:

setTimeout( function(){ while(true){} } , 100);
setTimeout( function(){ alert(’你好!’); } , 200);
setInterval( callbackFunction , 200);

第一行代码进入了死循环,但不久你就会发现,第二,第三行并不是预料中的事情,alert问候未见出现,callbacKFunction也杳无音讯!

这时你彻底迷惘了,这种情景是难以接受的,因为改变长久以来既定的认知去接受新思想的过程是痛苦的,但情事实摆在眼前,对JavaScript真理的探求并不会因为痛苦而停止,下面让我们来展开JavaScript线程和定时器探索之旅!

拔开云雾见月明

出现上面所有误区的最主要一个原因是:潜意识中认为,JavaScript引擎有多个线程在执行,JavaScript的定时器回调函数是异步执行的.

而事实上的,JavaScript使用了障眼法,在多数时候骗过了我们的眼睛,这里背光得澄清一个事实:

JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序.

JavaScript引擎用单线程运行也是有意义的,单线程不必理会线程同步这些复杂的问题,问题得到简化.

那么单线程的JavaScript引擎是怎么配合浏览器内核处理这些定时器和响应浏览器事件的呢?
下面结合浏览器内核处理方式简单说明.

浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步.假如某一浏览器内核的实现至少有三个常驻线程:javascript引擎线程,界面渲染线程,浏览器事件触发线程,除些以外,也有一些执行完就终止的线程,如Http请求线程,这些异步线程都会产生不同的异步事件,下面通过一个图来阐明单线程的JavaScript引擎与另外那些线程是怎样互动通信的.虽然每个浏览器内核实现细节不同,但这其中的调用原理都是大同小异.

由图可看出,浏览器中的JavaScript引擎是基于事件驱动的,这里的事件可看作是浏览器派给它的各种任务,这些任务可以源自JavaScript引擎当前执行的代码块,如调用setTimeout添加一个任务,也可来自浏览器内核的其它线程,如界面元素鼠标点击事件,定时触发器时间到达通知,异步请求状态变更通知等.从代码角度看来任务实体就是各种回调函数,JavaScript引擎一直等待着任务队列中任务的到来.由于单线程关系,这些任务得进行排队,一个接着一个被引擎处理.

上图t1-t2..tn表示不同的时间点,tn下面对应的小方块代表该时间点的任务,假设现在是t1时刻,引擎运行在t1对应的任务方块代码内,在这个时间点内,我们来描述一下浏览器内核其它线程的状态.

t1时刻:

GUI渲染线程:

该线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行.本文虽然重点解释JavaScript定时机制,但这时有必要说说渲染线程,因为该线程与JavaScript引擎线程是互斥的,这容易理解,因为JavaScript脚本是可操纵DOM元素,在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了.

在JavaScript引擎运行脚本期间,浏览器渲染线程都是处于挂起状态的,也就是说被”冻结”了.

所以,在脚本中执行对界面进行更新操作,如添加结点,删除结点或改变结点的外观等更新并不会立即体现出来,这些操作将保存在一个队列中,待JavaScript引擎空闲时才有机会渲染出来.或许这里你有个疑问了,为什么JS代码块的alert时界面有更新的,JS不是正在运行吗?其实当alert发生时,浏览器内核就会挂起JavaScript引擎线程,并促使界面执行了更新.

GUI事件触发线程:

JavaScript脚本的执行不影响html元素事件的触发,在t1时间段内,首先是用户点击了一个鼠标键,点击被浏览器事件触发线程捕捉后形成一个鼠标点击事件,由图可知,对于JavaScript引擎线程来说,这事件是由其它线程异步传到任务队列尾的,由于引擎正在处理t1时的任务,这个鼠标点击事件正在等待处理.

定时触发线程:

注意这里的浏览器模型定时计数器并不是由JavaScript引擎计数的,因为JavaScript引擎是单线程的,如果处于阻塞线程状态就计不了时,它必须依赖外部来计时并触发定时,所以队列中的定时事件也是异步事件.

由图可知,在这t1的时间段内,继鼠标点击事件触发后,先前已设置的setTimeout定时也到达了,此刻对JavaScript引擎来说,定时触发线程产生了一个异步定时事件并放到任务队列中, 该事件被排到点击事件回调之后,等待处理.
同理, 还是在t1时间段内,接下来某个setInterval定时器也被添加了,由于是间隔定时,在t1段内连续被触发了两次,这两个事件被排到队尾等待处理.

可见,假如时间段t1非常长,远大于setInterval的定时间隔,那么定时触发线程就会源源不断的产生异步定时事件并放到任务队列尾而不管它们是否已被处理,但一旦t1和最先的定时事件前面的任务已处理完,这些排列中的定时事件就依次不间断的被执行,这是因为,对于JavaScript引擎来说,在处理队列中的各任务处理方式都是一样的,只是处理的次序不同而已.

t1过后,也就是说当前处理的任务已返回,JavaScript引擎会检查任务队列,发现当前队列非空,就取出t2下面对应的任务执行,其它时间依此类推,由此看来:

如果队列非空,引擎就从队列头取出一个任务,直到该任务处理完,即返回后引擎接着运行下一个任务,在任务没返回前队列中的其它任务是没法被执行的.

可以试试运行以下例子作测试,第一个定时器是一个死循环,第二个定时器是向界面输出一串字符串,第一个定时器比第二个定时器要来得早.

setTimeout(function(){
for(;;);
}, 50);
setTimeout(function(){alert(’Hello’);}, 51);

运行后可看出,在没浏览器没提示脚本繁忙前第二个定时器alert是不会弹出的,因为单线程关系,第一个定时器回调脚本未返回前第二个脚本是没机会运行.

相信您现在已经很清楚JavaScript是否可多线程,也了解理解JavaScript定时器运行机制了,下面我们来对一些案例进行分析:

案例1:setTimeout与setInterval

setTimeout(function(){
   /* 代码块... */
   setTimeout(arguments.callee, 10);
}, 10);
 
setInterval(function(){
   /*代码块... */
 }, 10);

这两段代码看一起效果一样,其实非也,第一段中回调函数内的setTimeout是JavaScript引擎执行后再设置新的setTimeout定时, 假定上一个回调处理完到下一个回调开始处理为一个时间间隔,理论两个setTimeout回调执行时间间隔>=10ms .第二段自setInterval设置定时后,定时触发线程就会源源不断的每隔十秒产生异步定时事件并放到任务队列尾,理论上两个setInterval回调执行时间间隔<=10.

案例2:ajax异步请求是否真的异步?

很多同学朋友搞不清楚,既然说JavaScript是单线程运行的,那么XMLHttpRequest在连接后是否真的异步?
其实请求确实是异步的,不过这请求是由浏览器新开一个线程请求(参见上图),当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更事件放到JavaScript引擎的处理队列中等待处理,当任务被处理时,JavaScript引擎始终是单线程运行回调函数,具体点即还是单线程运行onreadystatechange所设置的函数.

封装Ajax系列之认识HTTP请求

七月 14th, 2009 由 Rock 发表

Ajax的好处就是在不刷新当前页面的情况下,可以默默的从浏览器后台异步发一个请求到服务器上,服务器响应该请求,并向浏览器返回一些有用的数据,浏览器处理脚本,即是所谓的回调,利用这些数据更新页面上应用。


我们把以上这个过程称为浏览器的一次异步HTTP请求。


要灵活使用jQuery的Ajax功能就得弄清楚什么是HTTP请求,以及请求是怎样响应的。
来看看完成这个过程需要什么条件:

1. 目标URL, 这个URL用于定位服务器上的资源。


2. 请求方式,POST或者GET。



初学者难以搞清POST与GET的区别,其实它们的差别主要在于浏览器端。

以GET方式发送的数据将被追加到URL中发送,常用来传递一些简单的数据,而实际上,URL不存在参数上限的问题,HTTP协议规范没有对URL长度进行限制。这个限制是特定的浏览器及服务器对它的限制。IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持。此外,由于数据放在URL中,所以会被缓存起来,故存在一定的安全风险。
以POST方式发送的数据作为HTTP消息的实体内容发送给服务器,并不是追加到URL中,故使用POST方式传递的数据量要比使用GET方式传送的数据量大的多。

3. 发送的数据。


这些数据往往来自表单中元素的数据,以键/值成对的方式组装,例如name=Rock&password=123。如果当前请求方式是GET,这些数据被追加到URL结尾;如果是POST方式,
数据将作为HTTP消息的实体内容发送。


4. 回调处理。


因为请求是异步执行的,所以你不清楚这个请求什么时候才能返回,就像寄出的一封信,你不知道什么时候有回信,直到邮递员自动把信送到家门口才得知。回调即是这样,请求发起前得向浏览器注册个“提醒”函数,当该请求完毕后返回数据后浏览器就调用执行刚才注册的函数,所以自然就得到了通知,可以对返回的数据进行处理。


世界是美好的,但不是完美的,我们希望次次请求都能顺顺利利,但应该也能容忍失败,回调也给了处理失败的机会,它会向回调函数传递几个状态,利用这些状态我们可以判断当前请求是成功还是失败的。


理解了HTTP请求处理所要的条件,再来看看浏览器为了完成这个请求给我们提供了什么样的方案。


XMLHttpRequest对象


浏览器内置的JavaScript异步传输对象XMLHttpRequest就是处理以上请求过程的完整实现。不过由于它太“底层”而利用起来比较麻烦。
以上处理过程几乎每一步都要明确的写出来。举个例子,要发送一个请求到服务器,询问服务器当前的时间,利用XMLHttpRequest对象处理为:

1. 获得XMLHttpRequest对象,IE与非IE浏览器获取方式不同。


2. 注册XMLHttpRequest对象的onReadyStateChange回调,也就是上面所说的“提醒”函数。


3. 调用对象的open方法,传递几个参数,method, url, async,即发送方式(GET/POST),URL与是否异步调用。
由于是请求返回时间,发送数据量很小,请求发送的信息放在URL中,这里method设为GET,URL假设为 /request?target=server_time


4. 调用对象的send方法,如果为POST并有数据,数据作为方法参数传递。


5. 处理回调函数中的状态。
在回调函数中必须判断请求的当前状态,成功或失败?
如果成功后读取XMLHttpRequest对象的responseText以获得服务器返回的数据。



如果你觉得上面过程比较简单,还可以接受,或者想练练自己的耐性,那么很好,多写几个应用试试,每写一个请求就得重复上面5个过程,我敢包不出第三个你就会觉得麻烦透了,是时候封装XMLHttpRequest了.

理清apply, call的区别与联系

七月 14th, 2009 由 Bgser 发表

如果没接触过动态语言,以编译型语言的思维方式去理解JavaScript将会有种神奇而怪异的感觉,因为意识上往往不可能的事偏偏就发生了,甚至觉得不可理喻.如果在学JavaScript这自由而变幻无穷的语言过程中遇到这种感觉,那么就从现在形始,请放下的您的”偏见”,因为这对您来说绝对是一片新大陆,让JavaScrip慢慢融化以前一套凝固的编程意识,注入新的生机!

好,言归正传,先理解JavaScrtipt动态变换运行时上下文特性,这种特性主要就体现在apply, call两个方法的运用上.

在没弄清楚原理之前,先送上一段背光经典,绝对原创哦

区分apply,call就一句话,

foo.call(this, arg1,arg2,arg3) == foo.apply(this, arguments)==this.foo(arg1, arg2, arg3)


call, apply都属于Function.prototype的一个方法,它是JavaScript引擎内在实现的,因为属于Function.prototype,所以每个Function对象实例,也就是每个方法都有call, apply属性.既然作为方法的属性,那它们的使用就当然是针对方法的了.这两个方法是容易混淆的,因为它们的作用一样,只是使用方式不同.


相同点:两个方法产生的作用是完全一样的

不同点:方法传递的参数不同


那什么是方法产生的作用,方法传递的参数是什么呢?

我们就上面的foo.call(this, arg1, arg2, arg3)展开分析.

foo是一个方法,this是方法执行时上下文相关对象,arg1, arg2, arg3是传给foo方法的参数.这里所谓的方法执行时上下文相关对象, 如果有面向对象的编程基础,那很好理解,就是在类实例化后对象中的this.


在JavaScript中,代码总是有一个上下文对象,代码处理该对象之内. 上下文对象是通过this变量来体现的, 这个this变量永远指向当前代码所处的对象中.


为了更好的领会这this是什么,举个例子.

—————————————————————————

//创建一个A类

function A(){

//类实例化时将运行以下代码

//此时的执行上下文对象就是this,就是当前实例对象

this.message = “message of a”;

this.getMessage = function(){

return this.message;

}

}

//创建一个A类实例对象

var a = new A();

//调用类实例getMessage方法获得message值

alert(a.getMessage());

——————————————————————————-


题外话:javascript对象所有属性都是公开的(public),没私有(private)之说,所以也可直接访问message属性

alert(a.message);


理解了this,再理解它的作用就容易了,我们再创建个类

//——

//创建一个B类

function B(){

this.message = ”message of b”;

this.setMessage = function(msg){

this.message = msg;

}

}

//创建一个B类实例对象

var a = new B();

//—

可见,A, B类都有一个message属性(面向对象中所说的成员),A有获取消息的getMessage方法,B有设置消息的setMessage方法,下面来显示call的威力.

//给对象a动态指派b的setMessage方法,注意,a本身是没有这方法的!

b.setMessage.call(a, “a的消息”);

//下面将显示”a的消息”

alert(a.getMessage());

//给对象b动态指派a的getMessage方法,注意,b本身也是没有这方法的!

这就是动态语言 JavaScript call的威力所在!

简直是”无中生有”,对象的方法可以任意指派,而对象本身一直都是没有这方法的,注意是指派,通俗点就是,方法是借给另一个对象的调用去完成任务,原理上是方法执行时上下文对象改变了.

所以 b.setMessage.call(a, “a的消息”); 就等于用a作执行时上下文对象调用b对象的setMessage方法,而这过程中与b一点关系都没有, 作用等效于a.setMessage( “a的消息”);

因为apply与call产生的作用是一样的,可以说

call, apply作用就是借用别人的方法来调用,就像调用自己的一样.



好,理解了call, apply相同处—–作用后,再来看看它们的区别,看过上面例子,相信您大概知道了.

从 b.setMessage.call(a, “a的消息”) 等效于 a.setMessage( “a的消息”) 可以看出, “a的消息”在call中作为一个参数传递,

那么在apply中是怎么表示的呢,直接解释说不清楚,apply要结合应用场景才一目了然.我们来设计一个应用场景:

function print(a, b, c, d){

alert(a + b + c + d);

}

function example(a, b , c , d){

//用call方式借用print,参数显式打散传递

print.call(this, a, b, c, d);

//用apply方式借用print, 参数作为一个数组传递,

//这里直接用JavaScript方法内本身有的arguments数组

print.apply(this, arguments);

//或者封装成数组

print.apply(this, [a, b, c, d]);

}

//下面将显示”背光脚本”

example(”背” , “光” , “脚”, “本”);

在这场景中, example方法内,a, b, c, d作为方法传递的参数, 方法分别运用了apply, call去借print方法来调用,

最后一句由于直接调用example方法, 所以在该方法中的上下文对象this就是window对象.

所以,call, apply方法它们除了第一个参数,即执行时上下文对象相同外,call方法的其它参数将依次传递给借用的方法作参数,而apply就两个参数,第二个参数为一个数组传递.所以可以说成


call, apply方法区别是,从第二个参数起, call方法参数将依次传递给借用的方法作参数, 而apply直接将这些参数放到一个数组中再传递, 最后借用方法的参数列表是一样的.


应用场景:

当参数明确时可用call, 当参数不明确时可用apply给合arguments

//例

print.call(window, “背” , “光” , “脚”, “本”);

//foo参数可能为多个

function foo(){

print.apply(window, arguments);

}

通用Combobox(Select)以至一般控件的设计与实现

六月 14th, 2009 由 Bgser 发表

本文涉及到combobox设计与实现上的一些方法论,并提供了一个参考实现.

Combobox(以下简称combox),也就是通常的意义上的Select控件,最具代表的是html里的select元素,
它可以在有限的空间内提供多个选项给用户选择,这个与菜单十分相似,并作为表单元素将数据提交到服务器.
种种原因使得有时有必要自定义一个combox控件,来代替自带的select,来分析一下html的select有什么基本功能.

>与form集成,字段可提交

>控件禁用或禁用单个选项

>选择,可以指定选择任意项或不选

>键盘导航
支持键盘上下键导航选项,ENTER,ESC键关闭下拉并响应

>滚动
如果列表项过多而下拉空间有限,就应该出现滚动条,
并且在用户选择选项过程中,选项要相应的滚动到可视范围内

>下拉框下拉时定位到可视范围内

>下拉框上下文切换时自动消失
即点击combox外时下拉框自动消失

如果是自定义的combox控件,可扩展功能有

>可编辑或不可编辑

>控件自定事件

>ajax加载
根据响应动态加载项

>过滤
用户在输入过程中过滤条项并自动定位

>定制下拉框
下拉框弹出的内容是可定制的,根据实现需要定制,如一个列表,一颗树,一个表格等

>在定制下拉框后combox的基本行为保持不应
自定下拉框后不能改变控件应用的基本行为,如选择,键导航等

>多样式支持,如控件的hover,点击,下拉时都有不同外观效果,下拉框具有阴影等

如果自定义combox控件,以上的基本功能是要实现的,不同方式有不同实现,越灵活就越不简单,.
现在考虑一个在封闭式环境下的实现,
这所谓的封闭环境是指为特定的需求应用而实现,代码考虑不重用,在响应过程中不与外界进行通讯,功能与控件完全一体化,具有很强的针对性实现,
这种实现较为简单,根据需求可部分实现select功能,有时为了设计与实现上的便利,用到多种的hack.
例如在HTML模板上的设计,可将下拉框的HTML组织在控制自身里,
而不是将下拉框的加在BODY中,这种设计简单明了,下拉定位方便,但也造成下拉框不够通用,在样式控制方面也要一定的技巧.
这类封闭环境下的产物常见有的一些自动完成控件,拿来就可以用.

下面来考虑一个通用的,可扩的设计方案
这里所谓通用,是指一个集成的环境里面,不同应用是共享的,代码是可重用的,来对上面combox所有功能分析一下,给控件解耦,提取出一些独立的功能,并给出一个与combox无关的控件设计模型.

一个控件模型可由以下一个或多个模型的组合而成
1.事件模型(event model)
具有事件模型的控件有添加事件监听器,发送,和移除事件的能力.
给控件建立事件模型是很有意义的,外部可监听控件一些感兴趣的事件并利用事件进行通讯,如MVC模型的实现多是基于事件(消息)的驱动.
实现了事件模型的控件具有可扩性,因为事件可有多个监听器,或者说通常意义上的回调,这个当然与单个回调效果不一样了.
有必要说说HTML事件模型与自定的不同,HTML受限于HTML结点而不是控件的事件,并且外界并不能向结点发送事件.

2.选择模型(selection model)
具有选择模型的控件具有单选,多选,并给合键盘导航选择等功能.

3.加载模型(loading model)
加载模型使得控件有自动Ajax加载并组装数据的能力

4.装饰模型(decoration model)
装饰模型与前面的几个模型不同,前面的通常是控件自身就支持,或是控件组成的不可或缺部分,而装饰模型不是控件自身就有的,它显得可有可无,只起来装饰效果,
而装饰时充分利用了其它的几个模型,例如在加载的时候给控件装饰一个”加载中”图标,利用事件模型作监听,利用加载模型加载前触发,在加载后移除效果.

此外不同应用还可独立出不同的模型,这里就不多说了.

通用的控件建好模型后,利用这些设计一个通用而功能齐全的combox控件.
combox控件根据视图划分为触发部分和下拉部分,其中下拉部分作为一个外部控件引入.
为了支持下拉的灵活实现,并且要求不同实现要具有一致的行为,下拉部分这控件必须支持上面所说的几个模型.
这样一来,Combobox就具有了大部分功能,并且行为始终保持一致.

实现参考:

http://www.bgscript.com/bgjs/samples/combo.html

讲一下实现参考例子里的内容:
里面的combox的下拉控件都实现了以上几种模型,
下拉框作为selector引入到combox,
selector在里面的分别是组控件,树型控件和小视图分组控件,
它们在combox中都表现出一致的行为,不加修改就可放到combox中.
下拉时出现loading图标和树型控件的loading图标是装饰模型实现后引起的效果,
它监听由加载模型触发的open,final事件并作出相应的处理.

利用getBoundingClientRect快速得到元素屏幕位置

六月 8th, 2009 由 Bgser 发表

getBoundingClientRect有何用

我们常常要计算一个元素相对屏幕客户区域位置,最常见的就是自定义的小提示,鼠标放到某个元素上,就在元素上方或下方显示提示.

一般的计算方法是累加元素offsetParent的offsetLeft和offsetTop一直到DOM的顶层,如果计算的元素是一个DIV,又没滚动,没边框等这些限制的话,

这方法大多数情况下可以返回想要的结果,为什么说是大多数情况呢,因为浏览器间的差异和元素的盒模型的不同使得计算这个相对屏幕客户区域位置是非常的麻烦,不得不用一堆怪异的代码和计算去处理怪异的模式,元素间稍有不同计算就有偏差,如在计算该值时以下可能出现的BUG就必须得考虑:

  • 计算 table的 offsets.
  • 计算 绝对定位的元素(position:absolute).
  • 在其它容器内出现的Scroll offsets(scrollLeft,scrollTop).
  • 所有父元素溢出的边框(overflow:visible).
  • 误算绝对定位的父元素.

如果用JS去实现这方法是一件相当不简单的事,想要正确计算,就得对不同浏览器对元素的定位方式之间的差异有充分的了解,这是一个大工程,相信要开一个讨论会讨论个牛年马月,再实践个十年八年才可正确的实现这么个方法,但如果浏览器本身就支持这么个方法那就是至妙的了,因为是浏览器本身支持的,它自己最清楚自己干了些什么,而且计算时用的是native code,速度超快,比用JS的性能提高不少,而我们要做的就是检查一下,再调用一下,非常方便!

实现跨浏览器,可行的getBoundingClientRect

虽然非常方便,但也不是拿来就可以用,即便是同一方法,在IE中还有怪异模式与标准模式间的差异,传说中是在ie中html元素是有border的,默认是2px,但这在怪异模式下是没有,也就是0.
这样就造成在调用方法后一个实际上是left:0,top:0的元素返回的确是left:2,top:2px.这样的误差是不能接受的.至于IE为什么这么做,为什么会多出2px.背光且不讨论这些公说公有理,婆说婆有理,不备具一般逻辑完备性的东西,还是留多点精力实现我们的WEB应用,找个完美的兼容方法才是正道.

非IE浏览器如果已实现getBoundingClientRect,它们都没什么问题,下面又为IE做个专题.

不声明DOCTYPE为strict模式时IE6,IE7默认是应用怪异模式,而IE8默认应用标准模式,但IE8在标准模式下通过声明

html,body{margin:0;}

所以解决方法:

1.不要声明DOCTYPE.

2.设置CSS.

html,body{margin:0;}

3.JavaScript实现,参考Ext Lib:

    //获得元素页面X,Y坐标
    function absoluteXY(el) {
     	   var p, b, scroll, 
                bd = (document.body || document.documentElement);
            //顶层?
            if(el == bd){
                return [0, 0];
            }
            //如果浏览器支持这大好方法,事件就简单了.
            if (el.getBoundingClientRect) {
                b = el.getBoundingClientRect();
                scroll = getScroll(document);
                return [b.left + scroll.left, b.top + scroll.top];
            }
 
            //否则就麻烦了,这回跑不掉了, 要用到终极计算法....
            var x = 0, y = 0;
 
            p = el;
 
            var hasAbsolute = getStyle(el,"position") == "absolute";
 
            while (p) {
 
                x += p.offsetLeft;
                y += p.offsetTop;
                f.view = p;
                if (!hasAbsolute && getStyle(p, "position") == "absolute") {
                    hasAbsolute = true;
                }
 
                if (CC.gecko) {
                    var bt = parseInt(getStyle(p, "borderTopWidth"), 10) || 0;
                    var bl = parseInt(getStyle(p, "borderLeftWidth"), 10) || 0;
                    x += bl;
                    y += bt;
                    if (p != el && getStyle(p, 'overflow') != 'visible') {
                        x += bl;
                        y += bt;
                    }
                }
                p = p.offsetParent;
            }
 
            if (CC.safari && hasAbsolute) {
                x -= bd.offsetLeft;
                y -= bd.offsetTop;
            }
 
            if (CC.gecko && !hasAbsolute) {
                f.view = bd;
                x += parseInt(getStyle(p, "borderLeftWidth"), 10) || 0;
                y += parseInt(getStyle(p, "borderTopWidth"), 10) || 0;
            }
 
            p = el.parentNode;
            while (p && p != bd) {
            		f.view = p;
                if (!CC.opera || 
                    (p.tagName != 'TR' && getStyle(p, "display") != "inline"))
               {
                    x -= p.scrollLeft;
                    y -= p.scrollTop;
                }
                p = p.parentNode;
            }
            return [x, y];
    }
 
  //辅助函数,得到元素滚动引起的偏移
  function getScroll(el){
        var d = el, doc = document;
        if(d == doc || d == doc.body){
            var l, t;
 
            if(CC.ie && CC.strict){
                l = doc.documentElement.scrollLeft ||
                    (doc.body.scrollLeft || 0);
                t = doc.documentElement.scrollTop || 
                    (doc.body.scrollTop || 0);
            }else{
                l = window.pageXOffset || 
                     (doc.body.scrollLeft || 0);
                t = window.pageYOffset || 
                     (doc.body.scrollTop || 0);
            }
            return {left: l, top: t};
        }else{
            return {left: d.scrollLeft, top: d.scrollTop};
        }
   }

OK,很好很强大.

更多参考信息:
http://msdn.microsoft.com/en-us/library/ms536433(VS.85).aspx
http://ejohn.org/blog/getboundingclientrect-is-awesome/
https://developer.mozilla.org/En/DOM:element.getBoundingClientRect

以上函数已在BackLight库中得到支持

JavaScript常用函数库详解

五月 22nd, 2009 由 Bgser 发表

在WEB开发中,javascript提供了许多函数供开发人员使用,这些函数在Ajax流行前足够了,但要构建一个交互性强些的应用恐怕就麻烦了,为此,收集了自己平时常用到一些JavaScript函数,它们在其它的JS库也常见,现在整理并附上注释,方便查阅,希望对大家有所帮助。
注:假设以下所有函数都放在一个CC对象中,方便引用。

  //这个方法相信是最常用的了,
  //它虽然没有选择器那么强大,但也有个小增强版,可查指定结点下ID所在的子元素
  function $(id, p) {
     //id是否是字符串,还是一个HTML结点
     var iss = id instanceof String || typeof id == "string";
     if (iss && !p)
         return document.getElementById(id);
     //如果是结点的话就直接返回该结点
     if(!iss)
        return id;
     //如果id与p是同一个元素,直接返回
     if(p.id == id)
        return p;
     //往父结点搜索
     var child = p.firstChild;
     while (child) {
        if (child.id == id)
            return child;
        //递归搜索
        var v = this.$(id, child);
        if (v)
           return v;
        child = child.nextSibling;
       }
    //的确找不到就返回null
    return null;
}
  //Prototype.js里经典的一个函数,将一个可枚举对象数据组成一个数组返回。
  //如arguments参数数组这对象,返回后就可用数组的方法操纵这些数据。
  function $A(iterable){
     if (!iterable) return [];
     if (iterable.toArray) {
        return iterable.toArray();
     } else {
        var results = [];
        for (var i = 0, length = iterable.length; i < length; i++)
            results.push(iterable[i]);
         return results;
     }
  }
each: function(object, callback, args) {
 
  if (!object) {
    return object;
  }
 
  if (args) {
    if (object.length === undefined) {
      for (var name in object)
      if (callback.apply(object[name], args) === false) break;
    } else for (var i = 0, length = object.length; i < length; i++) 
                if (callback.apply(object[i], args) === false) break;
  } else {
    if (object.length == undefined) {
      for (var name in object)
      if (callback.call(object[name], name, object[name]) === false) break;
    } else for (var i = 0, length = object.length, value = object[0];
                    i < length && callback.call(value, i, value) !== false; 
                    value = object[++i]) {}
  }
 
  return object;
}
//数组
function isArray(obj) {
  return (typeof obj === "array" || obj instanceof Array);
},
//字符串
function isString(obj) {
  return (typeof obj === "string" || obj instanceof String);
},
//函数
function isFunction(obj) {
  return (typeof obj === "function" || obj instanceof Function);
},
//数字类型
function isNumber(ob) {
  return (typeof ob === "number" || ob instanceof Number );
}
// 返回表单可提交元素的提交字符串.
// 例如 
//  <form>
//  <input type="text" name="user" value="rock" />
//  <input type="text" name="password" value="123" />
//  </form>
// 调用后就返回 user=rock&password=123
// 这些数据已经过encodeURIComponent处理,对非英文字符友好.
// form元素中如果没有name,则以id作为提供字符名.
function formQuery(f){
  // f,一个Form表单.
  var formData = "", elem = "", f = CC.$(f);
  var elements = f.elements;
  var length = elements.length;
  for (var s = 0; s < length; ++s) {
     elem = elements[s];
     if (elem.tagName == 'INPUT') {
         if ( (elem.type == 'radio' || elem.type == 'checkbox') && !elem.checked) {
              continue;
         }
     }
     if (formData != "") {
        formData += "&";
     }
     formData += encodeURIComponent(elem.name||elem.id) + "=" 
                  + encodeURIComponent(elem.value);
   }
    return formData;
 }
/**
 * 移除数组指定元素.
 * 参数既可传递一个整形下标,也可传递一个数组数据.
 */
Array.prototype.remove = (function(p) {
  //参数为下标
  if (CC.isNumber(p)) {
    if (p < 0 || p >= this.length) {
      throw "Index Of Bounds:" + this.length + "," + p;
    }
    this.splice(p, 1)[0];
    return this.length;
  }
  //参数为数组数据,最终要找到下标来操作
  if (this.length > 0 && this[this.length - 1] == p) {
    this.pop();
  } else {
    var pos = this.indexOf(p);
    if (pos != -1) {
      this.splice(pos, 1)[0];
    }
  }
  return this.length;
});
Array.prototype.indexOf = (function(obj) {
  for (var i = 0, length = this.length; i < length; i++) {
    if (this[i] == obj) return i;
  }
  return - 1;
});
/**
 * 万能而简单的表单验证函数,这个函数利用了JS动态语言特性,看上去很神秘,
 * 实际是很形象的,查看个例子就清楚了.
 */
validate: function() {
  var args = CC.$A(arguments),
  form = null;
  //form如果不为空元素,应置于第一个参数中.
  if (!CC.isArray(args[0])) {
    form = CC.$(args[0]);
    args.remove(0);
  }
  //如果存在设置项,应置于最后一个参数中.
  //cfg.queryString = true|false;
  //cfg.callback = function
  //cfg.ignoreNull
  //nofocus:true|false
  var b = CC.isArray(b) ? {}: args.pop(),
  d;
  var queryStr = b.queryString,
  ignoreNull = b.ignoreNull,
  cb = b.callback;
  var result = queryStr ? '': {};
  CC.each(args,
  function(i, v) {
    //如果在fomr中不存在该name元素,就当id来获得
    var obj = v[0].tagName ? v[0] : form ? form[v[0]] : CC.$(v[0]);
    //console.debug('checking field:',v, 'current value:'+obj.value);
    var value = obj.value,
    msg = v[1],
    d = CC.isFunction(v[2]) ? v[3] : v[2];
    //选项
    if (!d || typeof d != 'object') d = b;
 
    //是否忽略空
    if (!d.ignoreNull && (value == '' || value == null)) {
      //如果不存在回调函数,就调用alert来显示错误信息
      if (!d.callback) CC.alert(msg, obj, form);
      //如果存在回调,注意传递的三个参数
      //msg:消息,obj:该结点,form:对应的表单,如果存在的话
      else d.callback(msg, obj, form);
      //出错后是否聚集
      if (!d.nofocus) obj.focus();
      result = false;
      return false;
    }
    //自定义验证方法
    if (CC.isFunction(v[2])) {
      var ret = v[2](value, obj, form);
      var pass = (ret !== false);
      if (CC.isString(ret)) {
        msg = ret;
        pass = false;
      }
 
      if (!pass) {
        if (!d.callback) CC.alert(msg, obj, form);
        //同上
        else d.callback(msg, obj, form);
 
        if (!d.nofocus) obj.focus();
        result = false;
        return false;
      }
    }
    //如果不设置queryString并通过验证,不存在form,就返回一个对象,
    //该对象包含形如{elementName|elementId:value}的数据.
    if (queryStr && !form) {
      result += (result == '') ? 
         ((typeof obj.name == 'undefined' || obj.name == '') ? obj.id: obj.name) +
                  '=' + value: '&' + v[0] + '=' + value;
    } else if (!form) {
      result[v[0]] = value;
    }
  });
  //如果设置的queryString:true并通过验证,就返回form的提交字符串.
  if (result !== false && form && queryStr) result = CC.formQuery(form);
  return result;
}
/**
 * 应用对象替换模板内容
 * templ({name:'Rock'},'<html><title>{name}</title></html>');
 * st:0,1:未找到属性是是否保留
 */
templ: function(obj, str, st) {
  return str.replace(/\{([\w_$]+)\}/g, function(c, $1) {
    var a = obj[$1];
    if (a === undefined || a === null) {
      if (st === undefined) return '';
      switch (st) {
      case 0:
        return '';
      case 1:
        return $1;
      default:
        return c;
      }
    }
    return a;
  });
}

2009年06月16日已作微调

万能的JavaScript表单验证设计

五月 21st, 2009 由 Bgser 发表

平时用到的表单验证库,功能是异常的强大,但里面实际用到的却不多,为了几个验证就拖一个库,实在不值,不如自己设计个实用的函数解决这验证问题。
验证问题就是解决是还是不是的问题,是了就得提交,此外,用户体验也是个重点,正确,错误的消息怎么提示,所以,这个函数包括:

1.能对指定的多个字段进行验证
2.能够自定单个字段的验证方法
3.当验证失败时,可定义默认的消息提示方式
4.当验证失败时,可选择是否自动聚焦到元素
5.当验证成功时,可返回字段的提交字符串
6.字段元素既可传入ID也可传入name,name比ID要优先考虑
7.验证字段不依赖form

好了,这些功能够了吧,能应该任何的表单验证,如果有未能的,请在评论中指出,与我们分享,下面来看实现。
根据上面功能,独立出几个必要的函数。
实现用到的相关函数:

/**
 * 这个函数利用了JS动态语言特性,看上去很神秘,实际是很形象的,举个例子就明了.
 */
validate: function() {
  var args = CC.$A(arguments),
  form = null;
  //form如果不为空元素,应置于第一个参数中.
  if (!CC.isArray(args[0])) {
    form = CC.$(args[0]);
    args.remove(0);
  }
  //如果存在设置项,应置于最后一个参数中.
  //cfg.queryString = true|false;
  //cfg.callback = function
  //cfg.ignoreNull
  //nofocus:true|false
  var b = CC.isArray(b) ? {}: args.pop(),
  d;
  var queryStr = b.queryString,
  ignoreNull = b.ignoreNull,
  cb = b.callback;
  var result = queryStr ? '': {};
  CC.each(args,
  function(i, v) {
    //如果在fomr中不存在该name元素,就当id来获得
    var obj = v[0].tagName ? v[0] : form ? form[v[0]] : CC.$(v[0]);
    //console.debug('checking field:',v, 'current value:'+obj.value);
    var value = obj.value,
    msg = v[1],
    d = CC.isFunction(v[2]) ? v[3] : v[2];
    //选项
    if (!d || typeof d != 'object') d = b;
 
    //是否忽略空
    if (!d.ignoreNull && (value == '' || value == null)) {
      //如果不存在回调函数,就调用alert来显示错误信息
      if (!d.callback) CC.alert(msg, obj, form);
      //如果存在回调,注意传递的三个参数
      //msg:消息,obj:该结点,form:对应的表单,如果存在的话
      else d.callback(msg, obj, form);
      //出错后是否聚集
      if (!d.nofocus) obj.focus();
      result = false;
      return false;
    }
    //自定义验证方法
    if (CC.isFunction(v[2])) {
      var ret = v[2](value, obj, form);
      var pass = (ret !== false);
      if (CC.isString(ret)) {
        msg = ret;
        pass = false;
      }
 
      if (!pass) {
        if (!d.callback) CC.alert(msg, obj, form);
        //同上
        else d.callback(msg, obj, form);
 
        if (!d.nofocus) obj.focus();
        result = false;
        return false;
      }
    }
    //如果不设置queryString并通过验证,不存在form,就返回一个对象,
    //该对象包含形如{elementName|elementId:value}的数据.
    if (queryStr && !form) {
      result += (result == '') ? 
            ((typeof obj.name == 'undefined' || obj.name == '') ? obj.id: obj.name) +
                '=' + value: '&' + v[0] + '=' + value;
    } else if (!form) {
      result[v[0]] = value;
    }
  });
  //如果设置的queryString:true并通过验证,就返回form的提交字符串.
  if (result !== false && form && queryStr) result = CC.formQuery(form);
  return result;
}

OK,关键部分已完成, 举个例子说明:

/**
 * validate应用例子
 */
 
//测试邮箱格式
function isMail(strMail) {
  return /w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*/.test(strMail);
}
 
//密码长度>=6
function checkPassword(v) {
  return v.length >= 6;
}
 
//两次密码要相同
function isTheSame(v, obj, form) {
  return form ? form.password.value == v: CC.$('password').value == v;
}
 
//出错时自定回调
function myCallback(msg, obj, form) {
  alert("出错显示的消息是:" + msg + " - 元素:" + 
          obj.name + ",所在form:" + (form ? form.id: '无'));
}
 
//存在Form的例子
function testForm() {
  var result = CC.validate('testForm', 
     ['username', '请输入用户名。'], 
     ['mail', '邮箱格式不正确。', isMail], 
     ['password', '密码长度大于或等于6。', checkPassword],
     //完整的配置示例
     ['password2', '两次密码不一致。', isTheSame, 
        {nofocus: false,callback: myCallback,ignoreNull: false}
     ], 
     {queryString: true});
 
  if (result !== false) alert("恭喜,通过验证!提交的字符串是:" + result);
 
  return result;
}
//无Form的例子.
function testNoForm() {
  var result = CC.validate( //既然没form了,这里不必存入form id作为第一个参数.
   ['username', '请输入用户名。'], 
   ['mail', '邮箱格式不正确。', isMail], 
   ['password', '密码长度大于或等于6。', checkPassword],
   ['password2', '两次密码不一致。', isTheSame, 
        {nofocus: false, callback: myCallback, ignoreNull: false}
   ], 
  //函数最后一个参数
  { queryString: true});
 
  if (result !== false) alert("恭喜,通过验证!提交的字符串是:" + result);
 
  return result;
}

是不是很方便,一看就会用?
只需要验证想要的东西!
以上只是列举一部份功能,更多功能请浏览www.bgscript.com与验证相关的标签.

点击此处下载本文可运行示例。