高性能JavaScript——6、快速响应的用户界面

大多数浏览器让一个单线程共用于执行JavaScript和更新用户界面。每个时刻只能执行其中一种操作,这意味着当JavaScript代码正在执行时用户界面无法响应输入,反之亦然。当JavaScript代码执行时,用户界面处于“锁定”状态。管理好JavaScript的运行时间对Web应用的性能非常重要。

浏览器UI线程

用干执行JavaScript和更新用户界面的进程通常被称为“浏览器UI线程”(尽管对所有浏览器来说,称为“线程”不一定准确)。UI线程的工作基于一个简单的队列系统,任务会被保存到队列中直到进程空闲。一旦空闲,队列中的下一个任务就被重新提取出来并运行。这些 任务要么是运行JavaScript代码,要么是执行UI更新,包括重绘和重排。这个进程中最有趣的部分在于每一次输入可能会导致一个或多个任务被加入队列。

考虑一个简单的交互,点击按钮,屏幕上显示一条消息:

<button onclick="handleClick()">Click Me</button> 
<script type="text/javascript">
function handleClick(){ 
	var div= document.createElement("div")
	div.innerHTML = "Clicked"
	document.body.appendChild(div)
}
</script> 

当示例中的按钮被点击时,它会触发UI线程来创建两个任务并添加到队列中:

  • 第一个任 务是更新按钮的Ul
    它需要改变外观以指示它被点击了
  • 第二个任务是执行JavaScript
    包含handleClick()中的代码, 唯一被运行的代码就是这个方法和所有 被它调用的方法。

假设Ul线程处于空闲状态,第一个任务被提取出来执行更新按钮的外观,然后JavaScript任务被提取出来并执行。在运行过程中,handleClick()创建了一个新的<div>元素并把它附加在<body>元素末尾,这实际上引发了另一次UI变化。这意味着, 在JavaScript运行过 程中,一个新的UI更新任务被添加在队列中,当JavaScript运行完之后.UI还会再更新一次:
在这里插入图片描述

当所有Ul线程任务都执行完毕, 进程进入空闲状态,并等待更多任务加入队列。 空闲状态是理想的 因为用户所有 的交互都会立刻触发UI更新。

如果用户试图在任务运行期间与页面交互,不仅没有即时的UI更新,甚至可能新的UI更新任务都不会被创建并加入队列。事实上,大多数浏览器在JavaScript运行时会停止把新任务加入Ul线程的队列中, 也就是说JavaScript任务必须尽快结束, 以避免对用户体验造成不良影响。

浏览器限制

浏览器限制了JavaScript任务的运行时间。 这种限制是有必要的, 确保某些恶意代码不能通过永不停止的密集操作锁住用户的浏览器或计算机。
此类限制分两种:

  • 调用钱大小限 制
  • 长时间运行( long-running)脚本限制
    不同浏览器检测长时间运行脚本的方法会略有不同,Chrome没有单独的长运行脚本限制,替代做法是依赖其通用崩溃检测系统来处理此类问题

当浏览器的长时间运行脚本限制被触发时, 会弹出一个对话框提示用户, 而不管页面中其他的错误捕获代码 。这是一个主要的可用性问题, 因为大多数互联网用户并不精通技术, 因此会对出错信息感到迷惑, 不知道应该选择停止脚本还是允许它继续运行 。

如果 你的脚本在任意浏览器中触发此对话框 , 这意味着脚本花了太多时间来完成任务。 它还表明用户浏览器在JavaScript继续运行时 无法响应用户输入。 从开发人员的角度来看, 没办法改变一个长时间运行脚本对话框的外观;你检测不到它 , 因此不能用它来判断任何可能出现的问题。 显然, 处理长时间运行脚本的最佳方法是从一开始就避免它们

多久才算“太久”

浏览器 允许脚本持续运行好几秒, 但这并不意味着你也允许它这样做 。事实上, 为了更好的用户体验, 你的JavaScript 代码运行的持续时间应当远远小于浏览器的限制。 引用 JavaScript的创造者BrendanEich的话, “如果JavaScript运行了整整几秒钟, 那么很可能是你做错了什么…”

实证明 ,哪怕是一秒钟, 对脚本运行而言也太长了。 单个 JavaScript操作花费的总时间(最大值)不 应该超过100毫秒

Nielsen指出如果界面在100毫秒内响应用户输入,用户会认为自己在 “直接操纵界面中的对象”。超过100毫秒意味着用户会感到自己与界面失去联系。由于 JavaScript 运行时无法更新 Ul,所以如果 JavaScript 运行时间超过 100毫秒,用户就会感觉失去了对界面的控制。

更复杂的情况是有些浏览器在 JavaScript 运行时不会把 Ul 更新任务加入队列。例如,如果你在某些 JavaScript 代码运行时点击按钮,浏览器可能不会把重绘按钮按下状态的任务或点击按钮启动的新 JavaScript 任务加入队列。 最终结果是一个失去响应的 Ul,表现为 “挂起”或者“假死”。

各种浏览器的行为大致相同。当脚本执行时,Ul 不随用户交互而更新。 执行时间段内用户交互行为所引发的 JavaScript 任务被加入队列中,并在最初的 JavaScript 任务完成后依次执行。 而这段时间内由用户交互行为引发的 UI 更新会被自动跳过 , 因为页面中的动态变化部分会被优先考虑。因此, 在一个脚本运行期间点击一个按钮,将无法看到它被按下的样式, 尽管它的 onclick 事件处理器会被执行。

使用定时器让出时间片段

尽管你尽了最大努力,但难免会有一些复杂的 JavaScript 任务不能在100毫秒或更短时间内完成。这个时候,最理想的方能是让出 Ul 线程的控制权,使得 UI 可以更新。让出控制权意味着停止执行 JavaScript ,使UI 线程有机会更新,然后再继续执行 JavaScript。于是 JavaScript 定时器走进了我们的视野。

定时器基础

定时器与 UI 线程的交互方式有助于把运行耗时较长的脚本拆分为较短的片段。

第二个参数表示任务何时被添加到UI队列, 而不是一定会在这段时间后执行

定时器代码只有 在创建它的函数执行完成之后, 才有可能被执行。

如果setTimeout()的函数需要消耗比定时器延时更长的运行时间,那么定时器代码中的延时几乎是不可见的。

定时器的精度

JavaScript定时器延迟通常不太精准,相差大约几毫秒。因此,定时器不可用于测量实际时间

使用定时器处理数组

常见的一种造成长时间运行脚本的起因就是耗时过长的循环。 如果你已经尝试了之前介绍的循环优化技术, 但还是没能减少足够的运行时间, 那么你下一步的优化步骤就是选用定时器。 它的基本方法是把循环的工作分解到一系列定时器中

典型的简单循环模式如下:

for (var i=0, len=items.length; i < len; i++){ 
	process(items[i]); 
}

这类循环结构运行时间过长的原因主要是process()的复杂度或itemes的大小,或两者兼有。

是否可以用定时器取代循环的两个决定性因素:

  • 处理过程是否必须同步?
  • 数据是否必须按顺序处理?

基本的异步代码如下:

var todo = items.coneat(); //克隆原数组setTimeout(function(){ 
	//取得数组的下个元素并进行处理
	process(todo. shift()); 
	//如果还有需妥处理的元素, 创建另一个定时器 
	if( todo. length > 0 ){ 
		setTimeout(arguments.callee, 25); 
	} else { 
		callback(items); 
	}
},25)

可将该功能封装起来:

function processArray(items, process, callback){ 
	var todo = items.concat(); //克隆原数组
	setTimeout(function(){ 
		process(todo.shift()); 
		if (todo.length > 0){ 
			setTimeout (arguments.callee, 25); 
		} else { 
			callback (items); 
		}
	}, 25); 
}

每个定时器的真实延时时间在很大程度上取决于具体情况。 普遍来讲,最好使用至少25毫秒, 因为再小的延时,对大多数UI更新来说不够用。

使用定时器处理数组的副作用是处理数组的总时长增加了。 这是因为在每一个条目处理完成后UI线程会空闲出来,并且在下一条目开始处理之前会有一段延时。尽管如此, 为避免锁定浏览器 给用户带来的糟糕休验, 这种取舍是有必要的。

分割任务

我们通常会把一个任务分解成一系列子任务。 如果一个函数运行时间太长, 那么检查一下是否可以把它拆分为一系列能在较短时间内完成的子函数。 往往可以把一行代码简单地看成一个原子任务, 即使是多行代码, 也可以组合起来构成一个独立的任务。 某些函数已经可以很容易基于函数调用进行拆分。 例如:

function saveDocument (id){ 
	//保存丈档
	openDocument(id) 
	writeText(id); 
	closeDocument(id);
	//将成功信息史新至界面 
	updateUI(id); 
}

如果这个函数运行时间太长, 可以很容易地把它拆分成一系列更小的步骤, 把每个独立的方法放在定时器中调用。 你可以将每个函数都放入一个数组, 然后使用前一小节提到的数组处理模式:

function saveDocument(id){
	var tasks=[openDocument, writeText, closeDocument, updateUI]; 
	setTimeout(function(){
		//执行下一个任务
		var task = tasks. shift(); 
		task(id); 
		//检查是否还有其他任务
		if (tasks.length > 0){ 
			setTimeout(arguments.callee, 25); 
		}
	}, 25); 
}

正如数组处理那样,使用此函数的前提条件是:任务可以异步处理而不影响用户体验或造成相关代码错误。

记录代码运行时间

有时每次只执行一个任务的效率不高。考虑这种情况:如处理一个长度为1000项的数组,每处理一项需时l毫秒。如果每个定时器中只处理一项,且在两次处理之间产生25毫秒的延迟,这意味着处理数组需要的总时间为(25+ 1)×1000 = 26 000毫秒(或26秒)。如 果一次批处理50个,每批之间有25毫秒延迟呢?整个处理过程时间变成(1000/50)× 25 + 1 000 = 1 500毫秒(或1.5秒),而且用户不会觉察到界面阻塞,因为最长的脚本运行只持续了50毫秒。通常来说批量处理比单个处理要快。

如果你还记得JavaScript可以持续运行的最长时间为100毫秒,那么你可以优化先前的模式。建议把这个数字减半,不要让任何JavaScript代码持续运行50毫秒以上,这样做只是确保代码永远不会影响用户体验。

可以通过原生的Date对象来跟踪代码的运行时间。这是大多数JavaScript分析工具的工作原理。

function timedProcessArray(items, process, callback){ 
	var todo = items.concat(); //克隆原数组
	var start = +new Date(); 
	setTimeout(function(){ 
		do{
			process(todo.shift()); 
		} while (todo.length > 0 && (+new Date() - start < 50));
		
		if (todo.length > 0){ 
			setTimeout (arguments.callee, 25); 
		} else { 
			callback (items); 
		}
	}, 25); 
}

该函数添加了一个 do-while循环, 它在每个数组条目处理完后检测执行时间。 定时器函数执行时数组中始终会包含至少 一个条目,因此后测循环比前测循环更为合理。在Firefox 3 中,如果process()是个空函数,处理1000项的数组需要38~43毫秒 ; 原始的processArray()函数处理相同数组需要超过25 000毫秒。 这就是定时任务的作用, 能避免把任务分解成过 于零碎的片断。

定时器与性能

定时器会让你的JavaScript代码整体性能发生翻天覆地的变化, 但过度使用也会对性能造成负面影响。 本节中的代码使用了定时器序列,同一时间只有一个定时器存在, 只有当这个定时器结束时才会新创建 一个。 通过这种方陆法用定时器不会导致性能问题。

当多个重复的定时器同时创建往往会出现性能问题。因为只有一个UI线程,而所有的定时器都在争夺运行时间。

Thomas发现那些问隔在1秒或1秒以上的低频率的重复定时器几乎不会影响Web应用的H向应速度。这种情况下定时器延迟远远超过Ul线程产生瓶颈的值,因此可安全地重复使用。当多个重复定时器使用较高的频率(100到200毫秒之间)时,Thomas发现移动版Grnail应用会明显变慢,响应也不及时。

在你的Web应用中限制高频率重复定时器的数量。作为替代 方案,Thomas建议创建一个独立的重复定时器,每次执行多个操作。

Web Workers

自JavaScript诞生以来,还没有办法在浏览器Ul线程之外运行代码。WebWorkers API改变了这种状况,它引入了一个接口,能使代码运行且不占用浏览器U[线程的时间。作为 HTML5最初的一部分,WebWorkers APl已经被分离出去成为独立的规范。WebWorkers已经被Firefox3.5、Chrome3和Safari4原生支持。
Web workers给Web应用带来潜在的巨大性能提升,因为每个新的Worker都在自己的线程中运行代码。这意味着Worker运行代码不仅不会影响浏览器UI,也不会影响其他Worker中运行的代码。

Worker运行环境

由干WebWorkers没有绑定Ul线程,这也意味着它们不能访问浏览器的许多资掘。 JavaScript和UI共享同一进程的部分原因是它们之间互相访问频繁,因此这些任务失控会导致糟糕的用户体验。WebWorkers从外部线程中修改DOM会导致用户界面出现错误, 但是每个WebWorker都有自己的全局运行环境,其功能只是JavaScript特性的一个子集。

  • Worker运行环境组成:

    • 一个navigator对象,只包括四个属性:appName、appVersion、userAgent和platform。
    • 一个location对象 (与window.location相同, 不过所有属性都是只读的)。
    • 一个self对象, 指向全局worker对象 。
    • 一个importScripts() 方法, 用来加载Worker 所用到的外部JavaScript文件 。
    • 所有的ECMAScripr对象, 诸如:Object 、 Array、 Date等。
    • XMLHttpRequest构造器。
    • setTimeout() 和 setlnterval() 方法。
    • 一个close()方法, 它能立刻停止Worker运行 。
  • 创建方法:

    由于Web Worker有着不同的全局运行环境, 因此你无法从JavaScript代码中创建它。事 实上, 你需要创建一个完全独立的JavaScript文件, 其中包含了需要在 Worker中 运行的代码。 要创建网页工人线程, 你必须传入这个JavaScript文件的URL:

    var worker= new Worker"code.js") 
    

    此代码 一且执行, 将为这个文件创建一个新的线程和一个新的Worker 运行环境。 该文件会被异步下载, 直到 文件下载并执行完成后才会启动此Worker。

与Worker通信

//onmessage接收信息
var worker= new Worker("code. js"); 
worker.onmessage = function(event){ 
	alert(event.data); 
}
//通过postMessage()方住给Worker传递数据
worker.postMessage("Nicholas"); 


// code.js内部代码
self.onmessage = function(event){ 
	self.postMessage("Hello,"+ event.data)}

消息系统是网页和 Worker 通信的唯一途径。

  • 传递的数据类型限制
    只有特定类型的数据可以使用postMessage()传递。你可以传递原始值(字符串、 数字、 布尔值、 null和undefi ned), 也可以传递Object和Array的实例, 其他类型就不允许了。 有效数据会被序列化,传入或传出Worker, 然后反序列化。虽然看上去对象可以直接传入, 但对象实例完全是相同数据的独立表述。尝试传入一个不支持的数据类型会导致JavaScript 错误。

加载外部文件

//code.js内部代码
importScripts ("file1.js","file2.js"); 

实际应用

Web Workers适用于那些处理纯数据, 或者与浏览器Ul无关的长时间运行脚本。尽管它看上去用处不大, 但Web应用中通常有一些数据处理功能将受益于Worker而不是定时器。

  • 解析一个很大的 JSON
  • 编码/解码大字符串。
  • 复杂数学运算(包括图像或视频处理)。
  • 大数组排序。

任何超过100毫秒的处理过程, 都应当考虑:Worker方案是不是比基于定时器的方案更为合适。

小结

JavaScript和用户界面更新在同一个进程中运行, 因此一次只能处理一件事情。 这意味着当JavaScript代码正在运行时,用户界面不能响应输入, 反之亦然。 高效地管理UI线程就是要确保JavaScript不能运行太长时间,以免影响用户体验。 最后,请牢记如下几点:

  • 任何JavaScript任务都不应当执行超过 100毫秒。过长的运行时间会导致UI更新出现明显的延迟, 从而对用户体验产生负面影响。
  • JavaScript运行期间, 浏览器晌应用户交互的行为存在差异。 无论如何,JavaScript长 时间运行将导致用户体验变得混乱和脱节。
  • 定时器可用来安排代码延迟执行, 它使得你可以把长时间运行的脚本分解成一系列的小任务。
  • Web Workers是新版浏览器支持的特性,它允许你在Ul线程外部执行JavaScript代码, 从而避免锁定Ul。

Web应用越复杂, 积极主动地管理UI线程就越重要。 即使JavaScript代码再重要, 也不应该影响用户体验。

  • 32
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值