-
Notifications
You must be signed in to change notification settings - Fork 8
Description
如何在保证页面运行流畅的情况下处理海量数据
如何保证流畅
从用户的输入,再到显示器在视觉上给用户的输出,这一过程如果超过100ms,那么用户会察觉到网页的卡顿。
由于JS是单线程的,并且JS线程和UI渲染线程是互斥的,所以保证页面流畅的关键在于避免长耗时任务阻塞主线程。
W3C性能工作组在 LongTask规范 中也将超过50ms的任务定义为长任务。50ms这个阈值标准来源于 《RAIL Model》。
避免长任务的一种方案是使用Web Worker,将长任务放在Worker线程中执行,缺点是无法访问DOM,另一种方案就是下面要讲的时间切片。
时间切片及基础实现
时间切片是一种概念,也可以理解为一种技术方案,核心思想是:如果任务不能在规定时间内执行完,那么为了不阻塞主线程,这个任务应该让出主线程的控制权。
我们可以利用Generator 函数可以暂停执行和恢复执行的特性来实现时间切片。
// 任务列表
const tasks = [
() => 'task1',
() => 'task2',
() => 'task3',
]
// Generator函数
function *gen() {
for (const i in tasks) {
yield tasks[i]()
}
}
// 生成迭代器
const g = gen()
// 依次执行任务
g.next() // {value: "task1", done: false}
g.next() // {value: "task2", done: false}
g.next() // {value: "task3", done: false}
g.next() // {value: undefined, done: true}当然我们也可以只用一只循环来执行任务,但是如果我们要将任务分批执行,还需要手动记录任务执行到了哪一个,一遍下次继续上次的进度执行。使用Generator函数,生成的迭代器内部会记录状态,省去了我们自己记录的麻烦。
使用Generator函数的另一个好处是如果碰到另一个 Generator 函数(假设函数名为foo),可以使用yield* foo()将其融入我们的迭代队列。
function *foo() {
yield 'foo'
yield 'bar'
}
function *baz() {
yield* foo()
yield 'baz'
}
const g = baz()
g.next() // {value: "foo", done: false}
g.next() // {value: "bar", done: false}
g.next() // {value: "baz", done: false}
g.next() // {value: undefined, done: true}最终实现
下面是一个基于Generator和requestAnimationFrame的通用时间切片函数。
function timeSlice(tasks, during = 50) {
const g = gen(tasks) // 生成迭代器
const next = () => {
const startTime = performance.now()
let res
// 未执行完成且执行时间小于单次执行最大时间时,执行下一个任务
// 否则放入requestAnimationFrame,下次渲染前执行
//
do {
res = g.next()
} while (!res.done && performance.now() - startTime < during)
if (res.done) return
window.requestAnimationFrame(next)
}
window.requestAnimationFrame(next)
}
function *gen(tasks) {
for (const task of tasks) {
if (Object.prototype.toString.call(task) === '[object GeneratorFunction]') {
// `yield`的作用是:当task为Generator函数时,将其执行生成的迭代器嵌套展开
yield* task()
} else {
yield task()
}
}
}优化对比
同步执行
我们先来看一看一段同步代码的执行效果及表现图:
function task() {
const start = performance.now()
while (performance.now() - start < 1000) {
const p = document.createElement('p')
p.innerText = 'time slicing'
document.body.appendChild(p)
}
}
task()可以清楚的看到,所有的js都执行完毕后才进行渲染,会给用户造成卡顿感。
时间切片
然后看下将任务进行时间切片后的效果:
function *task() {
const start = performance.now()
while (performance.now() - start < 1000) {
const p = document.createElement('p')
p.innerText = 'time slicing'
document.body.appendChild(p)
yield
}
}
timeSlice([ task ])可以清楚看到将任务时间切片后,分为多段进行执行渲染,这样可以提升页面响应速度。
海量数据处理也可以采用时间分片的处理方式,可以将执行间隔可以设定得更小(以60帧为准,可设定为16ms),这样就可以基本保证不阻塞主线程、不影响页面流畅性。
其实时间切片并不是将页面总执行/渲染时间减少(相反会增加),而是通过更快地使用户看到变化、更快地响应用户输入来达优化效果。