大家都知道浏览器是一个单线程的,大多数浏览器让一个单线程共用于执行JavaScript和更新用户界面。每个时刻只能执行其中一种操作,这意味着当JavaScript 代码正在执行时用户界面无法响应输入,反之亦然。当JavaScript 代码执行时,用户界面处于“锁定”状态。管理好JavaScript 的运行时间对 Web应用的性能非常重要。
浏览器UI线程执行过程
这是一个简单的交互,点击按钮,会在body尾部添加一个内容:
以上代码遵循以下流程:
- 当
添加按钮
被点击时,它会触发UI线程来创建两个任务并添加到队列中。 - 第一个任务是更新按钮的UI,它需要改变外观以指示它被点击了。
- 第二个任务是执行JavaScript,包含handleClick()中的代码,唯一被运行的代码就是这个方法和所有被它调用的方法。
- 假设UI线程处于空闲状态,那么第一个任务被提取出来执行更新按钮的外观。
- 然后JavaScript任务被提取出来并执行。
- 在运行过程中,handleClick() 创建了一个新的<div>元素并把它附加在<body>元素末尾。
- 第六步导致引发了另一次UI变化。
- 一个新的UI更新任务被添加在队列中,即当JavaScript 运行完之后,UI还会再更新一次。
用流程图表示:
根据以上流程,当第5步执行JavaScript的时间过久,界面就得不到更新,就会有卡顿的现象,优化点就是这里。
优化点
Nielsen指出如果界面在100 毫秒内响应用户输入,用户会认为自己在“直接操纵界面中的对象”。超过100毫秒意味着用户会感到自己与界面失去联系。由于JavaScript 运行时无法更新UI,所以如果JavaScript 运行时间超过100 毫秒,用户就会感觉失去了对界面的控制。
那这种JavaScript执行过程中,在哪些业务场景下会有呢?这里我列举几个场景:
- 大量数据计算和处理
- 复杂的渲染逻辑
- 第三方库和框架的渲染和初始化
- 长任务队列
- 图像和视频处理
如果这种业务场景不进行性能优化,那最终结果是一个失去响应的UI,表现为“挂起”或“假死”状态。
那怎么优化JavaScript的处理时长呢?
方案
如果JavaScript任务执行过长,那么我们可以限制JavaScript任务在100毫秒或更短的时间内完成,以避免类似情况出现。但是有时候,尽管你尽了最大努力,但难免会有一些复杂的JavaScript 任务不能在100 毫秒或更短时间内完成。这个时候,最理想的方法是让出UI线程的控制权,使得UI可以更新。让出控制权意味着停止执行JavaScript, 使UI线程有机会更新,然后再继续执行JavaScript。于是我们可以用JavaScript的定时器来让出时间片段。
setTimeout
是 JavaScript 中使用非常广泛的一个函数,它允许我们在指定的延迟之后执行一段代码。这是一个异步操作,因为它不会立即执行代码,而是将代码的执行推迟到等待的时间过去之后,然后将需要执行的代码加到任务队列的后面。
使用定时器处理数组
常见的一种造成长时间运行脚本的起因就是耗时过长的循环。我们可以通过选用定时器。它的基本方法是把循环的工作分解到一系列定时器中。
我们可以写一个工具函数来处理长任务数组
function timedProcessArray(items, process, callback) {
const todo = items.concat(); // 克隆原始数组
setTimeout(function() {
const start = +new Date();
do {
process(todo.shift());
} while (todo.length > 0 && (+new Date() - start < 50));
if (todo.length > 0) {
setTimeout(arguments.callee, 25);
} else {
callback(items);
}
}, 25);
}
这个模式的基本思路是创建一个原始数组的克隆,并将它作为数组项队列来处理。第一次调用setTimeout() 创建一个定时器处理数组中的第一个条目。调用todo.shift()返回它的第一个条目然后把它从数组中删除。这个值作为参数传给process()。处理完后,检查是否还有更多条目需要处理。如果todo 数组中还有条目,那么就再启动一个定时器。因为下一个定时器需要运行相同的代码,所以第一个参数为arguments.callee。该值指向当前正在运行的匿名函数。如果不再有条目需要处理,那么调用callback() 函数。
上面的工具函数接受三个参数:待处理的数组,对每一个数组项调用的函数,处理完成后运行的回调函数。该函数的用法如下:
const items = [ 123,789,323,778,232,654,219 543,321, 160];
function outputValue(value){
console.log(value);
}
processArray(items, outputValue, function(){
console.1og("Done!");
});
上面的工具函数有以下限制:
- 处理过程是否必须同步?
- 数据是否必须按顺序处理?
如果这两个问题都是“否”,那么代码将使用于定时器分解任务。
我们可以模拟下长任务处理数组的场景:
结论
如果你在工作中有遇到类似于这种场景,那就赶紧将这个函数添加到你的utils里吧。
本文参考 高性能JavaScript 第六章节 快速响应的用户界面。