高性能javascript

原创
2014/04/30 11:40
阅读数 188

本文转自lifeleanote博客http://leanote.com/blog/view/53564ad01a91080383000000

参考 <高性能javascript> 这绝对是一本好书!!

无阻塞

js脚本放在html末尾

通过loadScript()加载与页面渲染无关的脚本, 延迟加载

作用域链

js的作用域是通过函数来决定的, 不存在if() {var a = ""};等其它{}封装的变量只能{}内用,  a不是局域作用域.

有一点需要注意: 函数作用域的嵌套关系是定义时决定的, 而不是调用时决定的, 也就 是说,JavaScript 的作用域是静态作用域, 又叫词法作用域,这是因为作用域的嵌套关系可 以在语法分析时确定, 而不必等到运行时确定。下面的例子说明了这一切:

var scope = 'top';
var f1 = function() { 
	console.log(scope);
}; 
f1(); // 输出 top
var f2 = function() { 
	var scope = 'f2'; 
	f1();
}; 
f2(); // 输出 top

局部变量最快, 最往外速度越慢, 所以可以将常用的全局变量缓存到局部变量中

执行一个函数, 它的作用域链如下:

函数执行过程中, 每遇到一个标识符, 都会从作用域链从头往尾找, 直到搜索为止或没找到.

优化点:

  • 缓存常用全局变量, 比如document

function initUI() {
	var doc = document; // 缓存之
	var bd = doc.body, 
	links = doc.getElementsByTagName("a"), 
	i= 0,
	len = links.length;
	while(i < len){
		update(links[i++]); 
	}
}

改变作用域链 with

function initUI(){
	var a = "local";
	with (document) { // 下面的作用域变了, [0]指向了document下的所有对象, 而a到了[1]了
		links = getElementsByTagName("a"),
		i= 0,
		len = links.length;
	}
}

try catch也可以改变作用域

闭包, 作用域和内存

闭包的作用: 保存上下文环境

var generateClosure = function() { 
	var count = 0;
	var get = function() {
		cunt ++; 6
		return count; 
	};
	return get; // 返回函数, 也包括函数所在的环境!
};
var counter = generateClosure(); 
console.log(counter()); // 输出 1 
console.log(counter()); // 输出 2 
console.log(counter()); // 输出 3

闭包不但包括被返回的函数,还包括这个函数的定义环境

function assignEvents(){
	var id = "xdi9592";
	// 闭包
	document.getElementById("save-btn").onclick = function(event){
		saveDocument(id); 
	};
}

当assignEvents()运行时作用域链:

闭包的副作用: assignEvents()运行结束后, 活动对象不会销毁, 因为闭包的作用域链还有一份引用!! 所以闭包需要更多的内存开销!!

当闭包执行时, 作用域链如下:


DOM

处理DOM代价很高

浏览器将DOM与javascript独立实现, 所以javascript操作DOM会跨过一条沟, 所以慢!

var a = document.getElementsByTagName("div");
Object.prototype.toString.call(a);
"[object HTMLCollection]"

HTMLCollection 是一个接口,表示 HTML 元素的集合,它提供了可以遍历列表的方法和属性。

HTML DOM 中的 HTMLCollection 是“活”的;如果基本的文档改变时,那些改变通过所有 HTMLCollection 对象会立即显示出来。

所以, 下面代码是个死循环:

var ps = document.getElementsByTagName("p");
for(var i = 0; i < ps.length; ++i) { // 每次都会重新计算length
	document.body.appendChild(document.createElement("p"));
}

reflow重排和repain重绘

reflow后发生repain

当DOM的变化影响了某元素的几何属性(高, 宽), 那么浏览器会使受影响的元素失效, 重新渲染树(reflow), 再显示出来(repain)

触发reflow:

  • DOM添加或修改

  • DOM尺寸发生变化(border, padding, margin, width, height)

  • 内容发生变化, 比如图片src变了, innerHTML += "lll"

  • 浏览器窗口发生变化

reflow和repain任务会加入UI线程队列, 除非你想得到布局信息, 比如:

  • offsetTop, offsetLeft

  • scrollTop, scrollLeft

此时会导致UI线程队列强制刷新, 立刻执行reflow和repain

最小化reflow和repain

一次性改变属性, 不要一点点改!

css("border-left", "1px");
css("width", "2px")

上面就执行了两次reflow

可以一次性将属性全改, 或通过改变class来改变样式(推荐)

可以先隐藏DOM, 修改完后再显示

或创建一个备份, 备份改完了, 替换之前的

var old = document.getElementById("ee");
var n = old.cloneNode(true);
n.appendChild(...);
old.parentNode.replaceChild(n, old); // 替换之

事件委派

避免过多的event bind, 减少内存, 浏览器会追踪每个事件处理器, 会占用很多内存.

event delegation的机制就是通过父来控制子, 事件冒泡到父, 再判断target.nodeName是否是子元素

p.onclick = function(e) {
	e = e || windows.e;
	var target = e.target || e.srcElement;
	if(target.nodeName == "a") {
		// 执行方法之
	}
}

算法和流程控制

循环

  • 数组遍历不要使用for-in

  • 把arr.length缓存起来, 不要每次都取, 因为arr.length是一次属性查找(是一次计算)

  • 要么减少循环次数, 要么减少每次循环的计算量

if-else

将概率大的放在最前, 可以使用二分法控制比较的个数

字符串和正则

字符串连接

str += "a" + "b" // 内存创建一个临时字符串 t = "ab" 再与str相加
str = str + "a" + "b" // 不必创建临时字符串, 直接加到str后面, 所以该方法效率高 [本地优化]
或以下和上面一样的高效
str += "a"
str += "b"

如果使用
str = "a" + str + "b" 就要创建临时字符串了

浏览器会尝试将左侧的字符串分配更多的内存便于连接其它字符串.

其它的方法, 比如, 都比上面的方法慢!!

["a", "b"].join("");
str.concat("a").concat("b")

正则优化

if(!String.prototype.trim) { // 如果浏览器已经支持了就不要自己写了, 原生的效率肯定高
	String.prototype.trim = function() {
		return this.replace(/^\s+/, "").replace(/\s+$/, "");
	}
}

避免使用|, 这样会有过多的选择分支

快速响应用户界面

UI线程运行了 javascript和UI渲染, 是单线程, 一次只能运行一种任务, 该线程维护一个队列, 用户的点击, 交互都是一种任务放置到队列中.

当运行javascript程序过长时, 此时UI线程只能等待该任务完成才能响应其它任务, 此时阻塞了UI的渲染.

当javascipt程序运行过长时, 可以使用setTimetout()来分解任务, 比如分出10个任务, 当运行第一个任务完后再setTimeout(function() {}, 0)第二个任务, 此时该任务会加到队列中, 此时UI线程可以响应用户的请求.

function go(arr) {
	setTimetout(function() {
		process(arr.shift()); // 处理第一个数组
		
		// 如果没处理完, 再处理
		if(arr.length > 0) {
			go(arr);
		}
	}, 10);
}

HTML5解决方案

使用WEB Workers

每个Web Worker都有自己的全局运行环境.

编程实践

避免使用eval运行字符串程序

有4中方式可以运行字符串:

eval("n1 + n2"); // 不要使用

new Function("arg1", "arg2", "return arg1+arg2"); // 不要使用

setTimeout("n1+n2", 1000); 或 setInterval() => 第一个参数使用function() {}

使用Object/Array直接量[]和{}

[], {}比new Array()和new Object()速度快, 代码量少

不要重复工作, 避免重复判断

比如 javascript的on和unbind:

function on(target, e, callback) {
	if(!target || !e || !callback) return;
	if(target.addEventListener) target.addEventListener(e, callback, false);
	else target.attachEvent("on" + e, callback); //IE
}
function unbind(target, e, callback) {
	if(!target || !e || !callback) return;
	if(target.removeEventListener) target.removeEventListener(e, callback, false);
	else target.detachEvent("on" + e, callback);
}

这里每次调用都要浏览器兼容性情况, 比较好的方法好下:

on = window.addEventListener ? 
	function(target, e, callback) {
		target.addEventListener(e, callback, false);
	}:
	function(target, e, callback) {
		target.attachEvent("on" + e, callback);
	}

或使用延迟决定

function on(target, e, callback) {
	if(!target || !e || !callback) return;
	if(target.addEventListener) {
		on = function(target, e, callback) { // 覆盖原方法
			target.addEventListener(e, callback, false);
		}
	} else { // IE
		on = function(target, e, callback) {
			target.attachEvent("on" + e, callback); 
		}
	}
	on(target, e, callback); // 调用之
}
展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部