【翻译】即学即用的Web前端优化技巧 ( Front-end optimizations you can start doing right now )

英文原文 http://odiseo.net/javascript/front-end-optimizations-you-can-do-right-now-on-your-existing-code thumb 强烈建议各位Web前端开发人员都要“真正”去学习Javascript并且掌握基本的DOM知识。诚然我也不得不赞同,不少基于Javascript特性的小把戏和小技巧根本就不会带来终端用户能明显觉察到的性能提升。话虽如此,我会在这篇文章里分享一些优化技巧,你可以马上在代码里使用这些技巧从而让程序跑的更快。而且从今以后,每当在项目里使用Javascript的时候你都应该想到并实践这些优化。

精明的使用选择器 Use selectors wisely

假设程序里有一个id为#profile-container的div, 你想在其内部选择一个或多个class为myClass的input元素。你可能会很快写出下面这样一个jquery选择器:

1
$('#profile-container input.myClass')

这句可以满足需求,但并不是最好的方法。事实上,$(‘#profile-container’).find(‘input.myClass’)会比上面那句更快。为什么呢?这需要了解下Jquery选择器机制。首先,$(‘#profile-container’)这个选择器可以很快的获得要选取的部分,这样当使用find()的时候,就会限制在一个很有限的查找范围,从而提升了性能。可以参开Jquery源码。 Rob Tarr的文章给出的一些测试结果可以印证这点,他还发现如果链式的使用find()逐层查找会更快,比如$(‘.container’).find(‘.main’).find(‘ul.list-1′).find(‘li’)

C9LBtwc.png

seesparkbox.com给出的选择器测试比较的结果

另一种精明的使用选择器提高性能的办法就是明确的的声明需要查找的元素类型。比如,如果我们知道要选择的元素类型的话,$(‘ul.todo’)明显会比$(‘.todo’)快的多。

缓存jQuery选择器的结果 Cache jQuery selector results

这实际上是一个总所周知的技巧,“不要重复”是经典的提升性能的准则和最佳实践。在Greg Franko精彩的幻灯片“jQuery最佳实践”里有一个有趣的例子:

1
2
3
4
5
6
// Set's an element's title attribute using it's current text
$(".container input#elem").attr("title", $(".container input#elem").text());
// Set's an element's text color to red
$(".container input#elem").css("color", "red");
// Makes the element fade out
$(".container input#elem").fadeOut();

问题就在于当我们每次调用$(“.container input#elem”)的时候,jQuery都需要使用一次选择器,也就是要遍历一遍所有的DOM。也是就说“.container input#elem”这个查找过程要重复执行4次!上面的代码应该修改成下面这样

1
2
3
4
5
6
7
8
// Stores the live DOM element inside of a variable
var elem = $("#elem");
// Set's an element's title attribute using it's current text
elem.attr("title", elem.text());
// Set's an element's text color to red
elem.css("color", "red");
// Makes the element fade out
elem.fadeOut();

注意这里很“抠门”的使用了选择器,因为我们要查找一个id,所以就没有必要去关心这个id是在某个class或者html元素里了。只需要#elem,这样即简单,也更快。

缓存.length属性 Cache .length property

在Javascript里,每次获取数组的.length属性的时候,都需要进行一次运算。因为,对于下面这样一段代码:

1
for (var i = 0; i < myArray.length; i++){...}

假设myArray数组大小是10000的话,每次循环计算一次, myArray.length的值就会重复计算10000次。所以你应该按下面这样写:

1
2
var arrayLength = myArray.length;
for (var i = 0; i < arrayLength; i++){...}

这样,只需要计算一次arrayLength就可以了。 这个真的这么重要么?其实在现代浏览器中,这两种的区别是微乎其微的,因为浏览器引擎已经做了这种优化。那么以后是否就完全不需要考虑这些了么?让我们看一下comp.lang.javascript新闻组的Thomas Lahn的观点(简单但比较哲学,译不出味道来,先放弃):

One should never rely on what one cannot know. You cannot know the runtime environments code once written will be exposed to. There is no good reason to assume the value of the “length” property should be automatically cached for a loop in the first place as it could change by statements in the loop. One can know instead that avoiding to access a property repeatedly whose value does not change, in favor of a local variable that holds this value, reduces code complexity. That has a chance – and it has been showed – to increase runtime efficiency, IOW to be faster. So it is wise to do that.

最小化DOM操作 Minimize DOM operations

写DOM是很“重”的操作。记住:DOM很慢。如果没意识到这点,你早晚会遇到性能问题。下面是一个让浏览器不堪重负的经典案例:

1
2
3
4
var toDoList = $("#todoList");
myTasks.forEach(function(task){
toDoList.append("<li id=" + task.index + ">" + task.name + "</li>");
});

在这里例子里面,我们每次forEach循环都会有读&写DOM的操作。实际上我们可以通过把这些节点存储在一个变量里,等到循环结束以后再一次性的把累积的变量写入DOM,从而规避这种混乱的情况。这样就只需要“附加”操作一次:

1
2
3
4
5
6
7
var toDoList = $("#todoList");
dynamicItems = "";

myTasks.forEach(function(task){
dynamicItems += "<li id=" + index + ">" + value + "</li>";
});
toDoList.append(dynamicItems);

避免重复创建对象 Avoid repeated object creation

如果在函数中创建了一个对象的话,那么这个对象在每次函数调用的时候都会被重复创建一次。这可能并不是你的本意,特别是当这个对象是静态的,你本来没准备在任何情况下改变它的时候。让我们看下David Walsh给出的例子

1
2
3
4
5
6
7
8
function cleanText(dirty) {
// Get rid of SCRIPT tags
clean = dirty.replace(/<script\[^>\]*>(\[\\s\\S\]*?)<\\/script>/gi, "");

// Do some more cleaning, maybe whitespace, etc.

return clean;
}

文字记号//实际上是new RegExp的缩写形式,也就是说每次使用类似/ab+c/这样的语句实际上就同时创建了一个正则表达式对象

1
2
3
//both expressions are equivalent
var re = /ab+c/;
var re = new RegExp("ab+c");

在本例中,我们其实并不需要每次都创建一个正则表达式对象。可以像这面这样通过在函数外创建对象,然后在函数中调用(through a closure scope–不太会翻译这句

1
2
3
4
5
6
7
8
9
var scriptRegex = /<script\[^>\]*>(\[\\s\\S\]*?)<\\/script>/gi;
function cleanText(dirty) {
// Get rid of SCRIPT tags
clean = dirty.replace(scriptRegex, "");

// Do some more cleaning, maybe whitespace, etc.

return clean;
}

委托事件监听 Delegate event listeners

给单独的元素使用事件监听会占用很多内存。如果给动态创建的很多新元素一一绑定事件监听的话代价会非常高。

1
2
3
document.querySelector('#todoList li').addEventListener("click", function() {
alert("Clicked on a task");
});

如果上面代码中的#todoList里有10000个li元素的话,也就是说会有10000个事件监听器。采用事件托管的方法可以就可以不用给每个单独的元素使用监听器,而是在这些元素的父节点上使用一个监听器就可以了。上面的例子可以修改成:

1
2
3
4
5
document.querySelector('#todoList').addEventListener('click', function(e) { 
if (e.target &amp;&amp; e.target.tagName == 'LI') {
alert("Clicked on a task");
}
});

用jQuery的话更简单了

1
2
3
$("#todoList").on("click", 'li', function() {
alert("Clicked on a task");
});

只需要使用选择器的话,别用jQuery了 Stop using jQuery when you only need selectors

总所周知, jQuery是遍历DEOM的最佳工具。但是如果你的项目只需要使用jQuery进行这一个工作的,就需要慎重权衡下是否值得因为这个需求而加载一个外部库文件。因为使用document.querySelectorAll也可以和使用jQuery一样实现这些基本的选择功能。是的,你没看错,完全可以使用浏览器内置功能实现像document.querySelectorAll(‘.content ul li’)这样的日常选择器操作。你说这样写看起来又长又丑?让我们看一下Burke Holland在他的文章5 Things You Should Stop Doing With jQuery里的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="container">
<ul>
<li id="pink">Pink</li>
<li id="salmon">Salmon</li>
<li id="blue">Blue</li>
<li id="green">Green</li>
<li id="red">Red</li>
</ul>
</div>

<script>
// create a global '$' variable
window.$ = function(selector) {
return document.querySelector(selector);
};

(function() {
// select item1 by id and change it's background color to salmon
var item = $("#salmon").style.backgroundColor="salmon";
console.log(item);
}());
</script>

这样写就可以继续使用你所钟爱的$(‘mySelector’)结构了。 那么querySelectorAll和jQuery的选择器相比究竟有多快呢?这要取决于不同的浏览器,但至少快5倍!尽管JQuery在document.querySelectorAll方法存在的情况下其实也是会自动调用它来实现选择器,但是我们是不是还要权衡下这多加载的90KB(jquery.mini.js文件)呢? 还想进一步学习么?我推荐进行在Javascript中更高效的进行分析和管理数据结构(profiling and managing data structures efficiently)这方面的学习。我以后可能会围绕这块写一些东西,也可以参考下这篇文章。 本文地址: http://awebird.com/blog/art/55

中转服务器JSONP跨域 & 使用CORS跨域

gg 由于项目中使用较多前后端分离,所以跨域成了经常要面对的问题。 (1)JSONP跨域,通过中转服务器中转 跨域的一种常用方式是jsonp,如果可以修改服务器端数据的话,需要js和服务器端同时处理对接,如果没有服务器端接口的修改权限,也可以在任意自己可以修改的服务器端的域里放一个中转文件url4ajax.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$server\_url=urldecode($\_GET\['url'\]);

$json=file\_get\_contents($server_url);

if(empty($_GET\['callback'\])){
echo $json; //ajax json
}elseif($\_GET\['notjson'\] && $\_GET\['notjson'\]=='1'){
echo trim($\_GET\['callback'\])."({'content':'".mysql\_real\_escape\_string(trim($json))."'})"; //ajax jsonp 返回数据不为json时,拼接成json
}else{
echo trim($_GET\['callback'\]).'('.$json.')'; //ajax jsonp
}

这样在客户端,可以通过$.getJSON获取原本不能跨域访问的数据接口(如下代码,为客户端通过$.getJSON经由www.midurl.com跨域访问www.dataurl.com的数据)

1
2
3
4
5
var DataUrl = "http://www.dataurl.com/xxxxxxx;
MidUrl = "http://www.midurl.com/url4ajax.php?url=" + encodeURIComponent( DataUrl ) + "&callback=?" ;
$.getJSON(MidUrl, function(data){
//doSomething
});

(2)CORS跨域 JSONP是跨域的常用手段,但是有个限制,就是参数传递是以http get的方式,这样在提交表单,特别是大数据量的情况下就不再适用。做手机端项目的时候,我们开始采用另一种跨域方法,基于CORS跨域,由于浏览器支持的情况,桌面端的网站还不能够采用这种方式。 改方法非常方便,其实只需要在服务器端加一行 header(“Access-Control-Allow-Origin: *”); 就可以解决 具体的介绍可以参考 http://blog.csdn.net/hfahe/article/details/7730944 下面是一个最简的例子 服务器端

1
2
3
4
<?php
$name=$_POST\['name'\];
header("Access-Control-Allow-Origin: *");
echo "haha,your name is ".$name;

客户端

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>测试CROS</title>
</head>
<body>
<label>输入姓名:</label><input id="user_input" type="text"></input>
<input id="submit_btn" type="button" value="点击发送" /><br>
<label>收到反馈:<label id="respose_text" style="color:red;"></label>
<script src="http://libs.baidu.com/jquery/1.9.0/jquery.js"></script>
<script type="text/javascript">
$('#submit_btn').click(function(){
$.ajax({
url:'http://www.serverurl.com/test_cors.php',
data:{name:$('#user_input').val()},
dataType:"text",
type:"POST",
success:function (result,textStatus){
$('#respose_text').text(result);
}
});
});
</script>
</body>
</html>

原文地址: http://awebird.com/blog/art/26

部分国行Android手机缺少谷歌GMS服务包导致HTML5 Geolocation无法定位的问题

phone 最近项目上用到HTML5的geolocation用于定位,用Chrome和手头的手机测试好好的功能,到终端用户那里反馈一些手机不能定位,最后确定出是部分三星、摩托的部分国行Android手机“阉割”了谷歌GMS服务包,导致HTML5的geolocation无法使用wifi和基站定位服务导致。 值得一提的是前期在stackoverflow和google groups里搜到对症描述的解决方案(貌似Android 2.*或者三星自身的问题)全部失效,因为介是个“中国特色”的问题-_lll 首先介绍下HTML5的Geolocation功能。通常情况下,我们是先判断浏览器是否支持geolocation, 如果不支持,可以提示错误,或者进入其他逻辑处理流程,现在移动端的智能手机浏览器绝大部分都是支持的。

1
2
3
4
5
6
7
8
if( navigator.geolocation ){
navigator.geolocation.getCurrentPosition(
updateLocation, handleLocationError,
{maximumAge:60000, timeout:50000, enableHighAccuracy:true}
);
}else{
alert( "对不起,您的浏览器不支持html5定位");
}

但是navigator.geolocation为true只是代表浏览器支持,浏览器还是要通过调用手机的定位功能来实现,所以上面的getCurrentPosition后面,分别后updateLocation和handleLocationError两个分支,分别对应成功而和失败的后续处理,这篇文章提到的由于缺少谷歌GMS服务包造成geolcation失效的情况就是走到了handleLocationError分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function handleLocationError(error) {
switch(error.code){
case 0:
alert("获取位置信息出错!");
break;
case 1:
alert("您设置了阻止该页面获取位置信息!");
break;
case 2:
alert("浏览器无法确定您的位置!");
break;
case 3:
alert("获取位置信息超时!");
break;
}
}

具体的返回值可以查手册,缺少谷歌GMS服务包的手机,会进入case 2,其实是“无法使用定位服务”。 写到这里,只是说明了原因,那有没有什么解决方案呢。搜索了下,原来提供地图和定位相关服务的不止是google一家,还有百度、高德、搜狗,下面介绍下百度的api (1)百度地图javascript API geolocation http://developer.baidu.com/map/jshome.htm 咋一看,百度js api也有自己的geolocation(http://developer.baidu.com/map/reference/index.php?title=Class:%E6%9C%8D%E5%8A%A1%E7%B1%BB/Geolocation) 这个取代html5自带的不就可以了么,经过实验,真是图样图森破了,原来所有的javascript API都还是调用的浏览器自身的geolocation进行封装实现的,也就是说如果原本不能wifi+基站定位,用百度、高德的javascript api的效果是一样的。 这里要说明的是,如果是开发Android原生软件的话,可以在APP里封装百度地图定位的SDK,这个是可以解决没有谷歌GMS服务包无法定位的问题的,因为百度地图定位SDK实际上是起到和谷歌服务包里面的定位模块一样的作用,由这里也可以看到HTML5 Webapp和原生APP的一个差别,不是一个层面的解决方案。 (2)使用百度的LocalCity() 根据IP定位到城市 既然上面说的HTML5自带的geolocation和百度Javascript API的geolocation都不能用了,那么就完全不能定位了么,我们这里暂时采用了一个方案,就是使用百度的LocalCity接口,进行IP定位,可惜这个只能返回城市和市中心的坐标,对于需要精确定位的LBS产品基本没有意义(根源上IPv4时代通过IP定位的想法本来就不是很靠谱吧),但是对我们的产品不失为一种可以接收的降级方案,当然用户体验上需要通过文字说明或者弹窗的形式告知用户一。

1
2
3
4
5
6
7
8
function myFun(result){
var latitude = result.center.lat;
var longitude = result.center.lng;

//doSomething(latitude,longitude);
}
var myCity = new BMap.LocalCity();
myCity.get(myFun);

(3)使用百度的IP定位API http://developer.baidu.com/map/ip-location-api.htm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var ajaxObj = createXHR(); 
ajaxObj.onreadystatechange = function() {
if (ajaxObj.readyState == 4) {
if ((ajaxObj.status >= 200 && ajaxObj.status < 300) || ajaxObj.status == 304) {
var jsonObj = eval("(" + ajaxObj.responseText + ")");
var point_x=jsonObj.content.point.x;
var point_y=jsonObj.content.point.y;
var axis = new BMap.MercatorProjection().pointToLngLat(new BMap.Pixel(point\_x,point\_y));

var latitude = axis.lat;
var longitude = axis.lng;

//doSomething(latitude,longitude);
}
}
};
ajaxObj.open("POST", "http://www.awebird.com/get_ip.php", true);
ajaxObj.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
ajaxObj.send("sendmessage=");

这理论上讲应该是个更好的解决方案,但是从实际使用测试来看,由于IP定位的局限性,实际上虽然返回的坐标不是市中心,但是也基本没有参考价值,所以只是把实现方法列一下,或许等到IPv6时代会有用吧。 这个api有几个值得注意的地方 (1)由于安全起见,所以Javascript是不支持直接获取IP地址的,所以需要服务器端,此外,如果手机程序是webapp或者和服务器不在一个域的话,还需要跨域支持,我们这里使用的是CORS跨域,get_ip.php如下(包括获取IP地址,和根据IP请求百度IP定位API获得城市信息)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$client_ip = getRealIpAddr();
$baidu\_result = file\_get\_contents('http://api.map.baidu.com/location/ip?ak=6227cb21d6ab31a87a5ae231f7xxxxxx&ip='.$client\_ip);
//注意上面的ak最后6位略去,需要使用自己免费注册的百度api的ak
header("Access-Control-Allow-Origin: *"); //CROS跨域
header("Content-Type:text/html; charset=utf-8");
echo $baidu_result;

function getRealIpAddr(){
if (!empty($\_SERVER\['HTTP\_CLIENT_IP'\])){
$ip=$\_SERVER\['HTTP\_CLIENT_IP'\];
}elseif (!empty($\_SERVER\['HTTP\_X\_FORWARDED\_FOR'\])){
$ip=$\_SERVER\['HTTP\_X\_FORWARDED\_FOR'\];
}else{
$ip=$\_SERVER\['REMOTE\_ADDR'\];
}
return $ip;
}

(2)百度IP定位API获得的坐标point需要经过坐标系转换成lat lng采用和其它接口公用 参见上面js里的

1
2
3
4
var axis = new BMap.MercatorProjection().pointToLngLat(new BMap.Pixel(point\_x,point\_y));

var latitude = axis.lat;
var longitude = axis.lng;

好了,本来还准备放一个DEMO的,暂时没有时间,以后可能会补充上来 原文地址: http://awebird.com/blog/art/24

本科毕设 - 电子技术虚拟实验(基于Flash + ActionScript)

flash 下面是我本科毕业设计时使用 Flash + ActionScript 做的电子技术虚拟实验。 回想起来,本科#电子科学与技术#四年,研究生#信号与信息处理#三年,最后工作后落入软件和互联网行业,特别是现在专职做Web码农,这3个月时间的毕设居然是跟现在工作最沾边的。 由于研二时候一次脑残的误操作,现在这些源码已经丢失,幸好后来在学校实验中心的网站上找到这些swf文件,重新整理了一下,有两点印象深刻 (1)UI… 不得不承认,所谓的“虚拟实验”,由于当时的时间(考研和NBA游戏间隙)和水平(特别是编程水平)的限制,基本上只有很少量的交互,大部分内容只是“下一步”引导出来的演示而已,也就算个高级点的PPT,但是直到现在看来,UI效果还是很不错的。想起来当时接到毕设课题的第一天,就去借个数码相机,把实验中心的电路板和元器件通通拍了一遍,然后最初好几天的工作,就是把电路板的照片放在图层最底,严格按照原尺寸位置还原。现在想想真有做互联网产品的劲头啊,可惜早了那么多年… (2)文件大小! 最后生成的swf文件最大的一个才80多K,包括图片、代码 大概就是一个jquery.mini.js的大小,现在想想真是震惊!看了一下,除了实验二的一个表格偷懒用了截图(不是强迫症看到也抓狂了)之外,剩下所有的文字(打散)、连线、元器件小到电阻上的一个个圈圈,全部采用矢量图绘制,这样不仅文件比较小,而且最好的就是不管到什么时候,采用什么高分辨率的大屏幕看都是一如的细腻效果。 这个毕设项目对我还是比较重要,虽然后面再也没有做过Flash相关的工作,但是这个项目基本做到自己满意的效果,所以尽管后来读研和第一份工作都没有做真正软件开发相关的工作,但是总对自己抱有一点信心,隐隐觉得自己适合并且注定有一天会去做这些事情。于是….. PS: 真心觉得Flash是个很好的技术,Adobe司真是废 其实围绕这个还有不少要说的,后面再慢慢补充吧。终于决定开始在空间慢慢积攒点东西了,就先把这7个swf挂在这里充充门面 电子科技大学 电子实验中心 网站(点击“仿真实验”,后面两个) http://125.71.228.222/wlxt/ncourse/xddzsy/web/jp/wlxt/wlxt.asp?wedid=wlxt&numb=5

  1. 实验一 常用数字逻辑门电路的研究
  2. 实验二 移位寄存器及其应用
  3. 实验三 触发器实现波形整形及脉冲延时的研究
  4. 实验四 555集成定时器的使用
  5. 实验五 数据选择和译码显示
  6. 实验六 电子秒表
  7. 实验七 集成运放波形产生电路

原文地址: http://awebird.com/blog/art/10

写在醉开始..

20130502_212157 由于Google Reader的关闭,整理订阅的过程中,顺便翻了翻以前的博客,发现写的好多东西居然都不记得了… 微博是信息时代的快餐,记录和表达的成本降低了,带来的是积淀不足。最近几年自己经历了这么多变化,毕业离校、学车、第一份工作、海外出差、离职、从零开始转行做web、新工作、爱情、家庭、朋友… 从成都到深圳、香港、斯里兰卡、南京、上海、青岛甚至还蹭着去美国溜达了一圈,却没有留下一篇文章,生活琐碎记在推特微博,知识乱七八糟的分布在evernote上,只有记录,没有思考和总结… 想到恰好有这么个拿来练手的空间和域名,与其荒废着,不如拿来写点东西,包括生活和工作,后面也会慢慢补充一点前面的事情,幸好过去还不算太久,而且可以自定义“发表时间” LOL 就这样开始吧…