分类目录归档:JavaScript

修复在IE6下img标签引用半透明png图片的bug

<!--[if lte ie 6]>
<script type="text/javascript">
    function correctPNG() {
        var images = document.getElementsByTagName('IMG');
        for (var i = 0, len = images.length; i < len; i++) {
            var img = images[i];
            if (img.className && img.className.indexOf('alpha-opacity') !== -1) {
                var imgID = (img.id) ? "id='" + img.id + "' " : "";
                var imgClass = (img.className) ? "class='" + img.className + "' " : "";
                var imgTitle = (img.title) ? "title='" + img.title + "' " : "title='" + img.alt + "' ";
                var imgStyle = "display:inline-block;" + img.style.cssText;
                if (img.parentElement.href) {
                    imgStyle = "cursor:hand; " + imgStyle;
                }
                var strNewHTML = "<span " + imgID + imgClass + imgTitle
                        + " style=\"" + "width:" + img.width + "px; height:" + img.height + "px;" + imgStyle
                        + "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader"
                        + "(src=\'" + img.src + "\', sizingMethod='scale');\"></span>";
                img.outerHTML = strNewHTML;
                --i;
                --len;
            }
        }
    }
    window.attachEvent("onload", correctPNG);
</script>
<![endif]-->

让IE6支持min-width

最新更新,在IE7中_width: expression中的表达示还会被计算,所以要考虑好脚本的兼容性。

最近和同事在搞一个自适应布局的页面改版,样式里用到了一些min-width,这个CSS属性在IE6里不被支持。要解决这个问题,网上大多数都是用IE的expression去解决的(不了解expression的同学看下http://msdn.microsoft.com/en-us/library/ms537634%28v=vs.85%29.aspx)。expression有个特点就是,当元素reflow/repaint的时候,它都会重复运算一次。虽说可以解决min-width的兼容问题,但是性能损耗太大了,而且页面上用的min-width越多,这个性能损耗会随之变大。下面是用expression实现min-width的代码

<style type="text/css">
#target {
	min-width: 800px;
	_width: expression(this.parentNode.clientWidth > 800 ? 'auto' : '800px');
	height: 100px;
	background: red;
}
</style>

为了解决用expression的性能损耗问题,我又考虑到用JavaScript去实现。思路很简单,就是把在expression里面的逻辑搬到window.onresize这个事件处理函数中,以达到重新布局的目的。但是问题又来了,想重新布局页面元素,就要拿到这个元素的句柄,如果是带有id的元素事情到不是很复杂,如果是在类选择器里用到了min-width,事情就不是那么容易了。虽然有jQuery之类的类库帮我们选择元素,但这也是一笔不小的性能开销,而且代码变得难于维护,要是日后对min-width有所更改的话,还要同时去改JS。

总结一下,碰到的问题有以下两点:

  1. 在CSS中用expression实现min-width会造成大量不必要的计算
  2. 在JS中用window.onresize重新设定元素宽度会涉及到:1),如何取元素句柄的问题;2),JS代码不够简洁难于维护,修改样式的同时还要改JS;

结合上面的分析,最好的情况就是在CSS定义里面为window绑定onresize事件处理函数,然后在函数里面对当前DOM元素实现重新布局。听起来不可思议的事情,在IE6中变得很容易,因为expression的本质就是一段js代码,在里面可以调用页面中声明的其它JS函数,可以写成_width: expression(someFunc(min-width-value));的形式。此处的someFunc是通过JS声明的函数,在里面实现对window.onresize的绑定,每当窗口改变的时候,将元素的宽度重新设置成min-width-value。在CSS定义中可以这么写 #target {min-width: 800px; _width: expression(someFunc(‘800px’)); } 样式的定义都放在一处,维护起来也容易,这样就解决了上述第二点的问题。

到目前为止,当元素需要reflow/repaint的时候,expression还是不断的去调用someFunc,这也是第一点问题。其实解决的办法很容易,就是在someFunc的最后一行调用 this.style.width=’auto’;这样在以后的重绘中width的值恒为auto,就不会重新计算expression了。至此所以问题都已解决。下面就是真实的代码了。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html;charset=GBK" />
	<title>测试IE6的min-width</title>
	<style type="text/css">
	#target {
		min-width: 800px;
		_width: expression(minWidth(this, 800)); 
		height: 100px;
		background: red;
	}
	</style>
</head>
<body>
	<div id="target"></div>
	<script type="text/javascript">
	(function() {
		var isIE6=!-[1,]&&!window.XMLHttpRequest,
			elements = [];
		
		function reflow(elem, value) {
			elem.style.width = elem.parentNode.clientWidth > value ? 'auto': value + 'px';
		}
		
		window.minWidth = function(elem, value) {
			if(isIE6) {
				reflow(elem, value);
				elements.push({
					'elem': elem,
					'value': value
				});
			} else {
				elem.style.width = 'auto';
			}
		};
		
		if(isIE6) {
			var timer;
			window.attachEvent('onresize', function() {
				var handler = arguments.callee;
				
				clearTimeout(timer);
				timer = setTimeout(function() {
					// 注销掉事件,防止reflow里触发onresize而导致的死循环
					window.detachEvent('onresize', handler);
					for(var i = 0, len = elements.length; i < len; i++) {
						var element = elements[i];
						reflow(element.elem, element.value);
					}
					window.attachEvent('onresize', handler);
				}, 50);
			});
		}
	}());
	</script>
</body>
</html>

有一点值得注意,就是如何在expression中拿到元素的句柄。

如何制作拖放后的惯性效果(一)

现在很多交互都需要这样一个效果,在拖拽结束时,被拖拽的元素会因为惯性向前移动一段距离。接下来说一下如何实现这个效果。

这个运动的原理其实很简单,就是一个匀减速运动。匀减速运动也就是匀加速运动的一种,只是加速度的方向和速度方向符号相反。我们在中学的时候学过匀加速运动的公式为:S = vt + (1/2)at^2;用文字表达就是在t时刻,物体移动的距离S为:初始速度v * t + (1/2) * 加速度a * t的平方。在这个公式中,我们可以将加速度a和初始速度v视为常量,将t当做变量,就可以知道在t时刻物体移动的距离了。那么问题又转到了如何对a和v求解。加速度a在这个问题中其实只是一个系数而已,我们可以设定a为1秒钟减少10000像素/秒的运动速度。(a = 10000)。那么到目前为止只有初始速度v是未知的了。

在这个动画中初始速度为我拖拽时松开鼠标那一刻的瞬时速度。我们知道计算平均速度的公式为v = s / t,如果这个t你取得足够的小,那么在t时间段移动的距离s除以t就是物体在某一点上的瞬时速度。接下来的问题就是如何确定这个t,以及如何确定在t时间段中物体移动的距离s。

我们知道在移动鼠标的时候会触发mousemove事件,而在持续移动鼠标的过程中,前后相邻的两个mousemove事件的时间间隔非常小,也就符合我们上面说的t时间段,在主流浏览器中event对象会有个timeStamp属性来标识这个事件是什么时刻发生的(在IE中因为要用window.event来取得事件,window.event没有timeStamp属性,所以要用new Date来计算事件发生的时刻),那么对t的求解就变得显而易见了:var t = e1.timeStamp – e0.timeStamp; 同时在t时间段移动的距离在X轴方向上为var s = e1.clientX – e1.clientX;

有了上面的思路,在鼠标松开的瞬时速度也就变得迎刃而解了。鼠标松开的瞬时速度v即为(e1.timeStamp – e0.timeStamp) / (e1.clientX – e1.clientX); e1为最后一次mousemove事件,e0为倒数第二次mousemove事件。如何保存e1和e0可以用以下代码:

var tracker = [];
document.body.addEventListener('mousemove', function(e) {
	tracker.push(e);
	if(tracker.length > 2) {
		tracker.shift();
	}
});

待续……

本地存储的兼容解决方案

IE浏览器用userData,主流浏览器用LocalStorage来解决本地存储的需求。userData存储的数据对于同一目录下的地址是可见的,如http://www.zhoumingzhi.com/1/foo.html可以访问到http://www.zhoumingzhi.com/1/bar.html存的数据。而LocalStorage存储的数据对相同域名下的所有页面都是可见的。

var localStorageAdapter = {
    
     storeName: 'NTESBBS',

     isLocalStorage: window.localStorage? true: false,

     dataDOM: this.isLocalStorage? null: (function() {
          try{
               var dataDOM = document.createElement('input'),
                    expires = new Date();

               dataDOM.type = 'hidden';
               dataDOM.style.display = 'none';
               dataDOM.addBehavior('#default#userData');
               document.body.appendChild(dataDOM);
              
               expires.setDate(expires.getDate() + 30);
               dataDOM.expires = expires.toUTCString();

               return dataDOM;
          } catch(ex) {
               return null;
          }
     })(),

     set: function(key, value) {
          var dataDOM = this.dataDOM;

          if(this.isLocalStorage) {
               window.localStorage.setItem(key, value);
          } else {
               if(dataDOM) {
                    dataDOM.load(this.storeName);
                    dataDOM.setAttribute(key, value);
                    dataDOM.save(this.storeName);
               }
          }
     },

     get: function(key) {
          var dataDOM = this.dataDOM;

          if(this.isLocalStorage) {
               return window.localStorage.getItem(key);
          } else {
               if(dataDOM) {
                    dataDOM.load(this.storeName);
                    return dataDOM.getAttribute(key);
               }
          }
     },

     remove: function(key) {
          var dataDOM = this.dataDOM;

          if(this.isLocalStorage) {
               window.localStorage.removeItem(key);
          } else {
               if(dataDOM) {
                    dataDOM.load(this.storeName);
                    dataDOM.removeAttribute(key);
                    dataDOM.save(this.storeName);
               }
          }
     }
}

Google+中URL的渐进增强

在高级浏览器下点击Google+的一些链接,并不是直接从服务器返回完整的页面,而是通过AJAX刷新页面的局部。

如在首页点击以下链接:

  • https://plus.google.com/stream
  • https://plus.google.com/photos
  • https://plus.google.com/photos
  • https://plus.google.com/circles

浏览器通过AJAX请求服务器返回数据,动态刷新页面上的一部分区域,这样做可以减少网络传输,快速响应,以达到更好的用户体验。同时我们注意 到,点击链接的时候,浏览器上的地址栏也做出了相应的调整,并且不会刷新整个页面。如点击“圈子”的时候,地址会变为 https://plus.google.com/circles。

这在早期浏览器版本下是做不到的,因为给window.location赋值会导致整个页面的刷新,除非是用改变URL的hash达到同样的目 的。Google+是用HTML5中history.pushState来实现替换当前的URL同时不刷新整个页面。

关于history.pushState的官方文档:https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history

Google+针对不同的操作场景和浏览器,做了一些方案处理点击链接后的行为。

  1. 如果直接在浏览器地址栏中输入地址回车,那么就会从服务器拿完整的页面呈现给用户。
  2. 如果浏览器不支持history.pushState,会降级用url hash的方式。
  3. 如果浏览器支持history.pushState,就用AJAX取区块的数据,然后生成HTML刷新到相应的区域上,并调用 history.pushState更新地址栏,生成访问历史。

值得注意的是,Google+对AJAX返回的数据做了缓存处理,再次请求的话会从缓存中获取数据。

nodejs中websocket传输大量数据

用nodejs当websocket server时,当客户端传输的数据量比较大时,数据会分片到达服务器,这时socket的data处理函数接收到的只是整个数据中的一部分,因此我们要将分片的数据拼合起来再做处理。当最后的数据到达时,数据的末尾是\ufffd,所以可以靠判断数据中是否包含\ufffd来指示整个数据是否已经全部接收。

var message = '';

socket.on('data', function(chunk) {   
    message += chunk;
    var index = message.indexOf('\ufffd');
    if(index !== -1) {
        console.log(message.slice(0, index)); 
        // console.log(message.slice(1, index)); 
        // Do your logic here
        message = message.slice(index + 1);
    }
});

快速清除多选框的已选中状态

工作中碰到一个变态的性能问题。CMS中有个页面,上面有个多选框,其中有14000个选项。页面中提供给用户一个按钮,点击这个按钮时要清除已选中状态。原有的代码是这样的:

function re() {
    for (var i = 0; i < document.form1.totopicid.options.length; i++) {
        document.form1.totopicid.options[i].selected = false;
    }
}

先抛开循环时多次计算length的问题不谈,光是执行14000次的document.form1.totopicid.options[i].selected = false;就要用户等很长时间。其实有一种更快捷的方式去做这件事情。代码如下:

function re() {
    var select = document.form1.totopicid;
    select.selectedIndex = 0;
    select.options[0].selected = false;
}

这样不仅代码量小,而且性能也能得到很大提升。

用JavaScript判断IE版本号

网上抄来的,找不到原文链接了。望告知。

// ----------------------------------------------------------
// A short snippet for detecting versions of IE in JavaScript
// without resorting to user-agent sniffing
// ----------------------------------------------------------
// If you're not in IE (or IE version is less than 5) then:
//     ie === undefined
// If you're in IE (>=5) then you can determine which version:
//     ie === 7; // IE7
// Thus, to detect IE:
//     if (ie) {}
// And to detect the version:
//     ie === 6 // IE6
//     ie > 7 // IE8, IE9 ...
//     ie < 9 // Anything less than IE9
// ----------------------------------------------------------
   
// UPDATE: Now using Live NodeList idea from @jdalton
   
var ie = (function(){ 
   
    var undef, 
        v = 3,
        div = document.createElement('div'),
        all = div.getElementsByTagName('i');
   
    while ( 
        div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->', 
        all[0]
    );
   
    return v > 4 ? v : undef; 
   
}());