Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

时间分片(Time Slicing) #22

Open
Vibing opened this issue Nov 27, 2019 · 0 comments
Open

时间分片(Time Slicing) #22

Vibing opened this issue Nov 27, 2019 · 0 comments

Comments

@Vibing
Copy link
Owner

Vibing commented Nov 27, 2019

时间分片

W3C性能工作组规定:将执行时间超过50ms任务定义为长任务(Long Task)。

长任务由于长时间阻塞主线程,会让用户感觉到卡顿。

而解决长任务的方式大致有两种:

  • 使用Web Worker,将长任务放在 Worker 线程中执行,缺点是无法访问 window 对象和 操作 DOM
  • 时间切片(Time Slicing)

什么是时间分片

时间分片并不是某个 api,而是一种技术方案,它可以把长任务分割成若干个小任务执行,并在执行小任务的间隔中把主线程的控制权让出来,这样就不会导致UI卡顿。

React 的 Fiber 技术核心思想也是时间分片,Vue 2.x 也用了时间分片,只不过是以组件为单位来实施分片操作,由于收益不高 Vue 3 把时间分片移除了。

使用时间分片

在早期,时间分片充分利用了“异步”来实现,例如:

btn.onclick = function (){
    someTask(); //50ms
    setTimeout(function() {
        otherTask(); //50ms
    })
}

上面代码,本来应该执行 100ms 的长任务,被拆分成了两个 50ms 的任务。

使用 Generator 函数

Generator是 ES6 里的语法,它提供了一个生成器函数来生成迭代器对象,我们利用 Generator 函数提供的 yield 关键字来让函数暂停,通过使用迭代器对象的 next 方法让函数继续执行。

如果我们用 Generator 函数,则可以这么写:

btn.onclick = ts(function* (){
    someTask();
    yield;
    otherTask();
})

这样就可以通过 yield 把一个长任务拆分成两个短任务。
我们也可以将 yield 关键字放在循环里:

btn.onclick = ts(function* (){
    while (true) {
        someTask();
        yield;
    }
})

上面虽然是个死循环,但依然不会阻塞主线程,所以浏览器不会卡死。

基于 Generator 函数实现 ts 方法

基于 Generator 函数的执行特性,我们很容易使用它来实现一个时间分片函数:

function ts(gen) {
  if (typeof gen === 'function') gen = gen();
  if (!gen || typeof gen.next !== 'function') return;
  return function next() {
    const res = gen.next();
    if (res.done) return;
    setTimeout(next);
  };
}

代码核心思想:通过 yield 关键字可以将任务暂停执行,并让出主线程的控制权;通过setTimeout将未完成的任务重新放在任务队列中执行。

演示

为了好理解,先写段长任务代码,将主线程阻塞一段时间:

const start = performance.now();
let count = 0;
while (performance.now() - start < 1000) {}
console.log('done!');

该段脚本霸占主线产长达 1s 的时间,如果把这段长任务分解成多个小任务执行呢。

我们通过这种方式来看一下,将长任务使用时间分片来处理:

ts(function* (){
    const start = performance.now();
    let count = 0;
    while (performance.now() - start < 1000) {
        yield;
    }
    console.log('done!');
})()

从图里看到,一个长任务虽然切成了诺干个小任务,但时间颗粒度过小,这样会导致执行任务的总时长增加,而W3C定义超过 50ms 为长任务,所以我们要控制一下任务时长,让它在一个合理的时间内,这样不会导致任务总时长过长。

继续优化

为了保证切割的任务接近 50ms,可以在 ts 函数中根据任务的指向时间判断是否应该一次性执行多个任务。
修改一下 ts 函数:

function ts(gen) {
  if (typeof gen === 'function') gen = gen();
  if (!gen || typeof gen.next !== 'function') return;
  return function next() {
    const start = performance.now();
    const res = null;
    do {
      res = gen.next();
    } while (!res.done && performance.now() - start < 25);
    if (res.done) return;
    setTimeout(next);
  };
}

上面代码中,做了一个 do while:如果当前任务执行时间低于 25ms 则多个任务一起执行,否则作为一个任务执行,一直到 res.done = true 为止。

现在,我们再测试一下看看结果:

可以看到,时间切片的颗粒度变的正常了,总时间也会相应缩短,完美!

总结

时间分片的概念以及技术方案让长任务分割成多个短任务,并且将控制权放给主线程,不会造成主线程卡顿

通过使用 Generator 函数特性,很方便的实现了 ts 函数。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant