首页 > 编程语言 >高性能JavaScript——6、快速响应的用户界面

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

时间:2024-03-25 16:01:24浏览次数:35  
标签:定时器 浏览器 用户界面 JavaScript 高性能 线程 UI 运行

大多数浏览器让一个单线程共用于执行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代码再重要, 也不应该影响用户体验。

标签:定时器,浏览器,用户界面,JavaScript,高性能,线程,UI,运行
From: https://blog.csdn.net/chtazycdf/article/details/136944702

相关文章

  • javascript三要素核验身份证号、姓名和人像是否匹配的身份证实名认证接口
    在开发的过程中,总会用到各种各样的API接口来实现各种各样的功能。互联网信息时代,为确保注册用户身份信息的正确性,无论是手机端还是电脑端应用都需要进行实名认证来防止虚假身份的使用,维护公共利益和个人权益的安全,实名认证已经成为了一个非常重要的环节。翔云身份证实名认......
  • 原生JavaScript写个表格版的日历
    如图:简单、易懂、写着玩儿的……代码如下:<!DOCTYPEhtml><htmllang="en"><head> <metacharset="UTF-8"> <metaname="viewport"content="width=device-width,initial-scale=1.0"> <title>Document&l......
  • JavaScript初识及基本语法详解
    JavaScript是一种高级的脚本语言,它在Web开发中扮演着至关重要的角色,主要用于增强用户与HTML页面的交互过程。以下是JavaScript的一些基本概念和语法要点:简介:JavaScript是三大Web技术之一(HTML、CSS和JavaScript),它能使网页从静态变为动态互动,广泛应用于Web应用开发。控制HTML:Jav......
  • 为什么使用类型化数组来进行字节操作而不是普通的 javascript 数字数组
    1.javascript中的数字数据类型默认为64位(8字节),无论任何数字。这意味着可以在不损失精度的情况下表示-2⁵³+1到2⁵³–1范围内的数字。这意味着即使我们想存储10个,也会消耗8个字节的内存,而这是根本不需要的。当内存效率是一个问题时,特别是在处理大型整数数组或二进制数......
  • 高性能、可扩展、支持二次开发的企业电子招标采购系统源码
     在数字化时代,企业需要借助先进的数字化技术来提高工程管理效率和质量。招投标管理系统作为企业内部业务项目管理的重要应用平台,涵盖了门户管理、立项管理、采购项目管理、采购公告管理、考核管理、报表管理、评审管理、企业管理、采购管理和系统管理等多个方面。该系统以项目......
  • 一文让你读懂JavaScript原型对象与原型链的继承
    前言有些新手朋友可能听说过这么一句话,就是js中存在两个链条,它们分别为:作用域链和原型链它们彼此的区别在于作用域链是为了访问变量和数据而存在的一种链条访问机制而原型链是访问对象的属性或者方法而存在的一种机制!其中这里的原型链就是今天我要说的主题!我们学习js必须......
  • JavaScript:void(0) 用法及常见问题解析
    JavaScript:void(0)用法及常见问题解析javascript:void(0);是一种在JavaScript和网页开发中经常使用的技术,尤其在处理链接的行为时。本文将深入探讨javascript:void(0);的用法,以及在使用过程中可能遇到的常见问题和解决方法。什么是javascript:void(0);?javascript:v......
  • javascript解析unicode字符,替换成正常字符
    开始使用正则表达式来匹配unicode字符以及html的特殊字符,比如空格,引号等。后面发现可以直接使用DOMParser进行转换,更加方便。需要先创建一个DOMParser对象,然后通过domParser对象使用parseFromString来进行解析,这样会把字符串中的Unicode和html特殊字符转换成正常显示的字符了。......
  • 任何样式,javascript都可以操作,让你所向披靡
    前言习惯了在css文件里面编写样式,其实JavaScript的CSS对象模型也提供了强大的样式操作能力,那就随文章一起看看,有多少能力是你不知道的吧。样式来源客从八方来,样式呢,样式五方来。chrome旧版本用户自定义样式目录:%LocalAppData%/Google/Chrome/UserData/Default/User......
  • JavaScript 排序算法
    在这篇文章中,我将介绍几种常见的JavaScript排序算法,并对它们的原理和实现进行详细说明。排序算法是计算机科学中非常重要的基础知识之一,它们可以帮助我们对数据进行有效的整理和排序,提高程序的效率和性能。冒泡排序(BubbleSort)冒泡排序是最简单的排序算法之一,它通过不......