在写chart widget的时候,我们使用了第三方的svg类库Raphael。结果客户给我们报了一个内存泄漏的bug,我们在测试的时候确实存在内存泄漏。经过测试发现了问题的原因。Raphael本身存在一些问题,还有就是在和jQuery使用的时候,我们也没有注意到一个问题。
在创建jQuery对象的时候,jQuery会将jQuery缓存起来,放到jQuery.cache对象中。在调用jQuery的remove方法的时候,会清理掉jQuery的缓存。我们的问题就出在这里,在Raphael中,自己有remove方法来清楚Raphael创建的raphael对象和svg/vml元素。而在chart中,我们会使用jQuery在raphael创建的svg/vml元素上。这样在调用raphael的remove方法的时候,就不能移除jQuery缓存。由此造成了对象一直保留内存中。
在Raphael中,自己的animate和remove方法也存在说明,就是在animate的时候,会创建animate需要的一些对象缓存到raphael对象中,在调用stop方法的时候会清理掉这些元素。而当remove的时候,仅仅做了remove的事情,并没有移除掉这些对象。而在文档中也没有提到需要注意的问题。我们在使用的时候,就没有注意到正在做动画的元素在remove的时候直接调用remove方法,没有在之前调stop。这样也造成了对象不能销毁。
上述两个问题是造成内存泄漏的主要原因。在试用jQuery的时候,如果要删除dom对象的话,最好采用jQuery的remove方法,否则可能会出现诸如我们那样的问题。在jQuery的remove方法中还会清楚掉时间绑定等信息。
jQuery1.7在事件上做了改动,将事件绑定统一为on和off了,对以前的绑定方式继续支持. 但是在文档中没有提到的改动有以下两个:
就是jQuery的die和undelegate方法,原来在取消命名空间绑定的时候不需要前面加点,但是在1.7中是需要前面加点。如下:
1.7以前
$(".selector").die("namespace");
$(".selector").undelegate(".selector1", "namespace");
1.7以后
$(".selector").die(".namespace");
$(".selector").undelegate(".selector1", ".namespace");
在asp.net中的webcontrol开发的时候采用render方法来呈现控件。在javascript中,对于一些html代码的拼接大多数情况下采用字符串相加的方式来处理,这样做有以下弊端:
1.可读性差,易出错,出错后不易发现。
2.在javascript最小化的时候,长长的字符串不能最下化。
于是在项目中我实现了累死render的方法来处理html代码的拼接。
StringBuilder = function () {
var self = this,
strs = [];
self.append = function (str) {
strs.push(str);
};
self.toString = function () {
return strs.join("");
};
self.dispose = function () {
strs = null;
};
},
TextElement = function (text) {
this.text = text;
this.render = function () {
return this.text;
};
},
HtmlElement = function (tagName, innerText, attributes) {
var self = this,
halfTags = { br: true, img: true, hr: true, input: true };
self.tagName = tagName || "div";
self.attributes = attributes || {};
self.innerText = innerText || "";
self.children = [];
if ($.isPlainObject(innerText)) {
self.innerText = "";
self.attributes = innerText;
}
self._isShortTag = false;
if (halfTags[self.tagName] === true) {
self._isShortTag = true;
}
self.render = function () {
var sb = new StringBuilder(),
html = "";
sb.append(self._renderBeginTag(self.tagName, self.attributes));
if (self.innerText !== "") {
sb.append(self.innerText);
}
if (self.children.length > 0) {
$.each(self.children, function (index, ele) {
sb.append(ele.render());
});
}
sb.append(self._renderEndTag(self.tagName));
html = sb.toString();
sb.dispose();
return html;
};
self.add = function (ele) {
this.children.push(ele);
};
self._renderBeginTag = function (tagName, attributes) {
var sb = new StringBuilder(),
strRet = "";
sb.append("<");
sb.append(tagName);
$.each(attributes, function (key, value) {
sb.append(" ");
sb.append(key);
sb.append("=");
sb.append("\"");
sb.append(value.toString());
sb.append("\"");
});
if (this._isShortTag) {
sb.append(" ");
} else {
sb.append(">");
}
strRet = sb.toString();
sb.dispose();
return strRet;
};
self._renderEndTag = function (tagName) {
var sb = new StringBuilder(),
strRet = "";
if (this._isShortTag) {
sb.append("/>");
} else {
sb.append("</");
sb.append(tagName);
sb.append(">");
}
strRet = sb.toString();
sb.dispose();
return strRet;
};
};
最上面上在javascript中实现了一个stringBuilder类。处理字符串拼接的。用户用的时候采用下面的方法来调用HtmlElement这个类
var div = new HtmlElement("div", {class:"divcss"});
var innertextbox = new HtmlElement("input", {type:"text"});
div.add(innertextbox);
var str = div.render();
str得到的结果是
<div class="divcss"> <input type="text" /> </div>
在使用的时候可以将创建textbox以及一些常用的html element封装成方法,这样就能提高代码的重用性,减少代码体积,也便于维护。
jquery 1.6已经发布了,在这个版本中最大的变化就是attr方法。原来我们通过这个方法取得jquery对象的属性。现在升级之后,原来采用attr方法的地方可能会出现问题。
新的attr方法只会取得DOM元素上设置的属性。而不会取得DOM元素内部的属性。比如说:
<input type="checkbox" checked="checked" />
在1.6版中,调用方法$(":checkbox").attr("checked")方法将返回"checked"值,而不是true. 而之前的版本则会返回true/false. 如果需要取得DOM的属性的时候,需要调用jquery提供的新方法prop(): $(":checkbox").prop("checked"). 在jquery 1.6中,这样设计应该主要上为了性能方面的考虑!
同时发现jquery的val()方法也许存在bug,就上不能对select元素取其值,取出的结果上undefined。但是官方文档上描述上可以的。
jQuery ui 内置了一组effects,包括:blind,bounce,clip,drop,explode,fade,fold,highlight,pulsate,puff,scale,size,shake,slide,transfer这些效果
这些effect可以通过$("selector").effect(effect,[option],[speed],[callback]),或者是jQuery中包装的show和hide方法调用。其参数和effect调用相同。
这里说明下参数的含义:
effect:就是效果名称,可以是上面列举的effects之一。
option:在不同的effect中包含不同的option。
speed:可以是数值,单位是ms,也可以是"slow","normal", 和 "fast"。
callback:就是执行完动画后的回调函数。
下面就每个effect的option做一个说明:
blind:
mode:值为hide/show
direction:值为vertical/horizontal
bounce:
direction: 值为up/down/left/right
distance: 值为数值,默认为20
times: 值为数值,默认为5
duration:值为数值,每个弹跳的速度,默认值250ms
clip:
mode:值为show/hide
direction:值为vertical/horizontal
drop
direction: 值为left/right/top/down
mode: 值为show/hide
distance:值为数值。
explode
pieces: 值为数字,爆炸的块数。默认为9
mode: 值为show/hide/toggle
fade
mode: 值为show/hide
fold
mode:值为show/hide
size: 值为数值,折叠的大小,默认为15
horizFirst:bool值,是否先水平方向折叠
highlight
mode: 值为show/hide
color: 值为颜色值,默认为#ffff99
pulsate
mode:值为show/hide
times:值为数值,跳动的次数
puff
mode:值为show/hide
percent: 值为数值,膨胀的比例,默认为150
scale
mode:值为show/hide
percent: 值为数值
direction:vertical/horizontal/both
origin:原始大小
from: 效果开始的大小
size
mode:值为show/hide
restore: bool类型,默认false
scale:vertical/horizontal/both
origin:原始大小
from:从一定的高度和宽度开始
to:到一定的高度和宽度结束
shake
mode:值为show/hide
direction: 值为left/right/up/down,默认值left
distance: 值为数字,默认值20
times:值为数字
duration:每次效果的速度
slide
mode:值为show/hide
direction: 值为left/right/up/down
distance: 值为数值
transfer
to:目标jQuery对象
className: 需要添加的css样式名称.
jQuery ui 为我们封装了上面的一些效果,在大部分的开发中,上述的效果基本可以满足我们的需求。而不需要我们自己另行开发。此外,在jQuery ui中封装了一组easing
可以在effect中搭配easing的使用,注意easing也是上述所有effect的option中的一个option,option的名称就是easing。
在开发widget的时候,发现了一个jquery ui position的一个bug。这个bug是有关collision的。这个功能就是当当前的定位的元素超出屏幕屏幕的时候,会自动翻转定位。比如说定位在右边,而右边不够显示定位元素的时候,就定位到左边。
下面我贴出代码来分析position的代码以及这个bug出现的位置。
/*
* jQuery UI Position 1.8.6
*
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Position
*/
(function( $, undefined ) {
$.ui = $.ui || {};
var horizontalPositions = /left|center|right/,
verticalPositions = /top|center|bottom/,
center = "center",
_position = $.fn.position,
_offset = $.fn.offset;
$.fn.position = function( options ) {
if ( !options || !options.of ) {
return _position.apply( this, arguments );
}
// make a copy, we don't want to modify arguments
options = $.extend( {}, options );
var target = $( options.of ),
targetElem = target[0],
collision = ( options.collision || "flip" ).split( " " ),
offset = options.offset ? options.offset.split( " " ) : [ 0, 0 ],
targetWidth,
targetHeight,
basePosition;
//如果相对定位的元素是document元素的话
if ( targetElem.nodeType === 9 ) {
targetWidth = target.width();
targetHeight = target.height();
basePosition = { top: 0, left: 0 };
// TODO: use $.isWindow() in 1.9
} else if ( targetElem.setTimeout ) {
targetWidth = target.width();
targetHeight = target.height();
basePosition = { top: target.scrollTop(), left: target.scrollLeft() };
} else if ( targetElem.preventDefault ) {
// force left top to allow flipping
options.at = "left top";
targetWidth = targetHeight = 0;
basePosition = { top: options.of.pageY, left: options.of.pageX };
} else {
targetWidth = target.outerWidth();
targetHeight = target.outerHeight();
basePosition = target.offset();
}
// force my and at to have valid horizontal and veritcal positions
// if a value is missing or invalid, it will be converted to center
$.each( [ "my", "at" ], function() {
var pos = ( options[this] || "" ).split( " " );
if ( pos.length === 1) {
pos = horizontalPositions.test( pos[0] ) ?
pos.concat( [center] ) :
verticalPositions.test( pos[0] ) ?
[ center ].concat( pos ) :
[ center, center ];
}
pos[ 0 ] = horizontalPositions.test( pos[0] ) ? pos[ 0 ] : center;
pos[ 1 ] = verticalPositions.test( pos[1] ) ? pos[ 1 ] : center;
options[ this ] = pos;
});
// normalize collision option
if ( collision.length === 1 ) {
collision[ 1 ] = collision[ 0 ];
}
// normalize offset option
offset[ 0 ] = parseInt( offset[0], 10 ) || 0;
if ( offset.length === 1 ) {
offset[ 1 ] = offset[ 0 ];
}
offset[ 1 ] = parseInt( offset[1], 10 ) || 0;
if ( options.at[0] === "right" ) {
basePosition.left += targetWidth;
} else if (options.at[0] === center ) {
basePosition.left += targetWidth / 2;
}
if ( options.at[1] === "bottom" ) {
basePosition.top += targetHeight;
} else if ( options.at[1] === center ) {
basePosition.top += targetHeight / 2;
}
basePosition.left += offset[ 0 ];
basePosition.top += offset[ 1 ];
return this.each(function() {
var elem = $( this ),
elemWidth = elem.outerWidth(),
elemHeight = elem.outerHeight(),
marginLeft = parseInt( $.curCSS( this, "marginLeft", true ) ) || 0,
marginTop = parseInt( $.curCSS( this, "marginTop", true ) ) || 0,
//此处就是出现问题的地方,在ie下面如果当前元素的margin没有设置或者设置为auto的话,在取$.curCss(this,"marginRight",true)的时候会去取出为
//"auto",此问题在其他的浏览器下,不会取出为auto,而是当前元素的css实际值。,在转换的时候会为NAN,这个时候再做前面的运算,都将为NAN,
//最终此值将被计算为0. 而这样,在下面做反转的时候,将会计算出错误的值,而不会发生发转。目前的解决方法就是在当前的元素上加上
//margin-right和margin-bottom的值。
collisionWidth = elemWidth + marginLeft +
parseInt( $.curCSS( this, "marginRight", true ) ) || 0,
collisionHeight = elemHeight + marginTop +
parseInt( $.curCSS( this, "marginBottom", true ) ) || 0,
position = $.extend( {}, basePosition ),
collisionPosition;
if ( options.my[0] === "right" ) {
position.left -= elemWidth;
} else if ( options.my[0] === center ) {
position.left -= elemWidth / 2;
}
if ( options.my[1] === "bottom" ) {
position.top -= elemHeight;
} else if ( options.my[1] === center ) {
position.top -= elemHeight / 2;
}
// prevent fractions (see #5280)
position.left = parseInt( position.left );
position.top = parseInt( position.top );
collisionPosition = {
left: position.left - marginLeft,
top: position.top - marginTop
};
$.each( [ "left", "top" ], function( i, dir ) {
if ( $.ui.position[ collision[i] ] ) {
$.ui.position[ collision[i] ][ dir ]( position, {
targetWidth: targetWidth,
targetHeight: targetHeight,
elemWidth: elemWidth,
elemHeight: elemHeight,
collisionPosition: collisionPosition,
collisionWidth: collisionWidth,
collisionHeight: collisionHeight,
offset: offset,
my: options.my,
at: options.at
});
}
});
if ( $.fn.bgiframe ) {
elem.bgiframe();
}
elem.offset( $.extend( position, { using: options.using } ) );
});
};
//此处就是做翻转的函数的。
$.ui.position = {
fit: {
left: function( position, data ) {
var win = $( window ),
over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft();
position.left = over > 0 ? position.left - over : Math.max( position.left - data.collisionPosition.left, position.left );
},
top: function( position, data ) {
var win = $( window ),
over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop();
position.top = over > 0 ? position.top - over : Math.max( position.top - data.collisionPosition.top, position.top );
}
},
flip: {
left: function( position, data ) {
if ( data.at[0] === center ) {
return;
}
var win = $( window ),
over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft(),
myOffset = data.my[ 0 ] === "left" ?
-data.elemWidth :
data.my[ 0 ] === "right" ?
data.elemWidth :
0,
atOffset = data.at[ 0 ] === "left" ?
data.targetWidth :
-data.targetWidth,
offset = -2 * data.offset[ 0 ];
position.left += data.collisionPosition.left < 0 ?
myOffset + atOffset + offset :
over > 0 ?
myOffset + atOffset + offset :
0;
},
top: function( position, data ) {
if ( data.at[1] === center ) {
return;
}
var win = $( window ),
over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop(),
myOffset = data.my[ 1 ] === "top" ?
-data.elemHeight :
data.my[ 1 ] === "bottom" ?
data.elemHeight :
0,
atOffset = data.at[ 1 ] === "top" ?
data.targetHeight :
-data.targetHeight,
offset = -2 * data.offset[ 1 ];
position.top += data.collisionPosition.top < 0 ?
myOffset + atOffset + offset :
over > 0 ?
myOffset + atOffset + offset :
0;
}
}
};
// offset setter from jQuery 1.4
if ( !$.offset.setOffset ) {
$.offset.setOffset = function( elem, options ) {
// set position first, in-case top/left are set even on static elem
if ( /static/.test( $.curCSS( elem, "position" ) ) ) {
elem.style.position = "relative";
}
var curElem = $( elem ),
curOffset = curElem.offset(),
curTop = parseInt( $.curCSS( elem, "top", true ), 10 ) || 0,
curLeft = parseInt( $.curCSS( elem, "left", true ), 10) || 0,
props = {
top: (options.top - curOffset.top) + curTop,
left: (options.left - curOffset.left) + curLeft
};
if ( 'using' in options ) {
options.using.call( elem, props );
} else {
curElem.css( props );
}
};
$.fn.offset = function( options ) {
var elem = this[ 0 ];
if ( !elem || !elem.ownerDocument ) { return null; }
if ( options ) {
return this.each(function() {
$.offset.setOffset( this, options );
});
}
return _offset.call( this );
};
}
}( jQuery ));