这是我参与8月更文挑战的第14天,活动详情查看: 8月更文挑战
前言
JavaScript代码的执行通常会阻塞页面的渲染,考虑到用户体验,这就会限制我们在编写代码时需要注意减少或避免一些执行时间过长的逻辑运算。本文记录常见的几种场景和解决方案
浏览器的限制
由于JavaScript是单线程的,这就意味着浏览器的每个窗口或页签在同一时间内,要么执行JavaScript脚本,要么响应用户操作刷新页面,也就是说这二者的行为是相互阻塞的。例如JavaScrpt化码正在执行时,用户页面会处于锁定状态无法进行输入,如果JavaScrpt代码执行时间过长,显然会给用户带来糟糕的体验。
对于浏览器的这种限制,我们可能就需要对长时间运行的脚本进行重构,尽量保证一段脚本的执行不超过100ms,如果超过这个时间阈值,用户明显就会有网站卡顿变慢的使用体验。 引起JavaScrpt-f时间过长的原因多种多样,但十分常见的原因概括起来有三类:第一类是对DOM的频繁修改,相比于JavaScript脚本的运算,DOM操作的开销都是极高的,这也是现代前端框架中普遍采取虚拟DOM的原因。
所谓虚拟DOM就是将真实的DOM抽象为JavaScript对象.用户交互和数据运算可能会带来DOM频繁修改,但这其中大部分的修改操作可能对最终呈现给用户的页面来说都是中间过程,所以就将这些大量的中间过程交由JavaScript处理,处理完成后统一再去修改真实的DOM,这样便尽可能多的降低对真实DOM的修改频率。
第二类是不恰当的循环,可能因为循环次数执行过多,或者每次循环中执行了过多操作,若能将功能尽可能分解就会明显缓解这个问题。
第三类是存在过深的递归。
前面章节有提到过浏览器对JavaScript调用栈存在限制,将递归改写成迭代能有效地避免此类问题。
异步队列
JavaScript既要处理运算又要响应与用户的交互,它是如何完成的呢?答案是异步队列。
当我们创建一个异步任务时,它其实并没有马上执行,而是被JavaScript引擎放置到了一个队列中,当执行完成一个任务脚本后,JavaScript功擎便会挂起让浏览器去做其他工作,比如更新页面,当页面更新完成后,JavaScript引擎便会查看此异步队列并从中取出一个任务脚本去执行,只要该队列不为空,这个过程便会不断重复,当队列中的任务脚本执行完后,JavaScript引擎便处于空闲状态,直到有新的任务脚本进入该异步队列。
据此我们便有了对执行过长任务的一种优化策略,即将一个较长的任务拆分为多个异步任务,从而让浏览器给刷新页面留出时间,但过短的延迟时间也可能会让浏览器响应不及时,因为在几毫秒的时间里无法止确完成页面的更新与显示,通常可以使用定时器来控制一个100ms左右的延迟,同事定时器也是JavaScript中创建一个加入异步队列十分有效的方法:
// 将一个对大数组的处理过程拆成多个异步队列
function chunk(arr, process){
setTimeout(() => {
// 去除目标数组中一部分进行处理
const item = arr.shift()
process(item)
if(arr.length > 0){
// 一次延迟处理接下来的数据
setTimeout(arguments.callee, 100)
}
}, 100)
}
这个例子只是为了说明起见,让每个异步任务仅处理数组中的一个数据集,对于较大规模的数组,可预先规定单次异步任务的处理数量,然后拆分数据集,依次加入异步队列进行处理。