JS 分片任务的高阶函数封装

简介: 这篇文章介绍了在实际业务开发中,如何处理渲染大量元素导致的页面卡顿问题。文章通过问题复现,分析了浏览器渲染原理,并提出了使用分片任务执行的方法。文章详细介绍了如何使用requestIdleCallback API实现分片任务,并封装成高阶函数,以便开发者针对不同任务进行自定义分片处理。同时,文章也涉及了浏览器渲染原理和requestIdleCallback API的用法。

前言

在我们的实际业务开发场景中,有时候我们会遇到渲染大量元素的场景,往往这些操作会使页面卡顿,给用户带来非常不好的体验,这时候我们就需要给任务分片执行。

场景复现

我们看一段代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>分片任务的高阶函数封装</title>
  <style>
    .ball {
      width: 100px;
      height: 100px;
      background: #f40;
      border-radius: 50%;
      margin: 30px;
    }

    .ball1 {
      position: absolute;
      left: 0;
      animation: move1 1s alternate infinite ease-in-out;
    }

    @keyframes move1 {
      to {
        left: 100px;
      }
    }
  </style>
</head>

<body>
  <button class="btn"">点击按钮向页面插入 100000 条数据</button>
  <div class=" ball ball1"></div>
    <script>
      const btn = document.querySelector(".btn");
      const datas = new Array(100000).fill(0).map((_, i) => i);
      btn.onclick = () => {
        // 记录开始时间
        console.time('耗时');
        for (const i of datas) {
          const div = document.createElement("div");
          div.innerText = i;
          document.body.appendChild(div);
        }
        console.timeEnd('耗时');
      }
    </script>
</body>

</html>

效果:

这里的小球动画是为了演示浏览器的卡顿效果,我们可以看到,直接向页面插入 100000 条元素,页面会直接卡死,等渲染完才正常,为什么会这样呢?

问题本质

这是因为浏览器的渲染频率一般是 60Hz,也是目前主流显示屏的频率。60Hz 表示计算机一秒钟要渲染 60 帧画面,所以渲染一帧或者说每一片段所需时间就是 1/60 = 16.6ms

但是现在这个任务的执行时间比较长,导致这个任务没有给浏览器留出足够的空间进行渲染,导致渲染任务延后执行,这就是页面卡死的原因。这就涉及到浏览器的渲染原理了,有关这方面的知识可以看下我之前发过的文章:现在浏览器的渲染原理及流程

问题分解

我们已经知道问题产生的原因,那我们怎么优化呢?解决办法就是给这些任务分片执行,让任务一段一段执行,这样我们就看不到页面卡顿了。

这里我们就涉及到两个问题:

  1. 下一次分片什么时候开始?

  2. 每一次分片执行多少

这里我们就要用到一个 APIrequestIdleCallback

这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

代码优化

我们要实现一个分片函数,让它帮我们执行任务:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>分片任务的高阶函数封装</title>
  <style>
    .ball {
      width: 100px;
      height: 100px;
      background: #f40;
      border-radius: 50%;
      margin: 30px;
    }

    .ball1 {
      position: absolute;
      left: 0;
      animation: move1 1s alternate infinite ease-in-out;
    }

    @keyframes move1 {
      to {
        left: 100px;
      }
    }
  </style>
</head>

<body>
  <button class="btn"">点击按钮向页面插入 100000 条数据</button>
  <div class=" ball ball1"></div>
    <script>
      const btn = document.querySelector(".btn");
      const datas = new Array(100000).fill(0).map((_, i) => i);
      btn.onclick = () => {
        // 记录开始时间
        console.time('耗时');
        performChunk(datas)
        console.timeEnd('耗时');
      }

      const performChunk = (datas) => {
        // 边界判定
        if (datas.length === 0) {
          return;
        }
        let i = 0;
        // 开启下一个分片的执行
        function _run() {
          // 边界判定
          if (i >= datas.length) {
            return;
          }
          // 一个渲染帧中,空闲时开启分片执行
          requestIdleCallback((idle) => {
            // timeRemaining 表示当前闲置周期的预估剩余毫秒数
            while (idle.timeRemaining() > 0 && i < datas.length) {
              // 分片执行的任务
              const div = document.createElement("div");
              div.innerText = i;
              document.body.appendChild(div);
              i++
            }
            // 此次分片完成
            _run()
          })
        }
        _run();
      }
    </script>
</body>

</html>

核心代码就是 requestIdleCallback 函数,我们看下效果:

这里就没有明显的卡顿效果了,这是因为浏览器把这些任务分片执行了,比如说前面执行100次,然后再接着执行100次,所以现在的渲染效率就比较高了。

封装高阶函数

这里我们已经完成这项任务的分片执行,但我们希望这个分片函数针对的不仅仅是这个任务,它应该是通用的,所以我们要提取一些操作,让用户自己去执行,比如说:

  1. 具体任务的执行过程

  2. 分片任务执行的时机?每一次具体分片多少?

  3. 如果是 node 环境呢?

这些都应该由开发者去定义,基于这些,我们封装成一个更加通用的高阶函数:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>分片任务的高阶函数封装</title>
</head>

<body>
  <button class="btn"">点击按钮向页面插入 100000 条数据</button>
    <script>
      const btn = document.querySelector(".btn");
      btn.onclick = () => {
        // 自定义执行过程
        const taskHandler = (_,i) => {
          const div = document.createElement("div");
          div.innerText = i;
          document.body.appendChild(div);
        }
        broswerPerformChunk(10000, taskHandler)
      }

      const performChunk = (datas, taskHandler,scheduler) => {
        if(typeof datas === 'number'){
          datas = {
            length:datas
          }
        }
        // 边界判定
        if (datas.length === 0) {
          return;
        }
        let i = 0;
        // 开启下一个分片的执行
        function _run() {
          // 边界判定
          if (i >= datas.length) {
            return;
          }
          // goOn 判断是否还继续执行
          scheduler((goOn) => {
            // timeRemaining 表示当前闲置周期的预估剩余毫秒数
            while (goOn() && i < datas.length) {
              // 分片执行的任务
              taskHandler(datas[i], i);
              i++
            }
            // 此次分片完成
            _run()
          })
        }
        _run();
      }

    /**
     * @description: 浏览器执行环境
     */  
    const broswerPerformChunk = (datas, taskHandler) => {
      const scheduler = (task) => {
          requestIdleCallback((idle) => {
            task(() => idle.timeRemaining())
          })
        }
        performChunk(datas, taskHandler,scheduler)
    }    
    </script>
</body>

</html>

总结

通过本篇文章我们可以学习到什么?

  1. requestIdleCallback API 的用法

  2. 浏览器的渲染原理

  3. 分片高阶函数的封装

相关文章
|
11月前
|
JavaScript
如何动态执行 JS 脚本
这篇文章介绍了在业务开发中动态执行JS脚本的几种方法,包括使用eval()函数、setTimeout、动态创建script标签以及使用Function构造函数。这些方法各有特点,如eval()同步执行且作用域为当前执行作用域,动态script标签同步执行且作用域为全局作用域等,具体实现方案需根据业务场景选择。
331 0
|
11月前
|
运维 监控 Cloud Native
从本土到全球,云原生架构护航灵犀互娱游戏出海
内容整理自「 2025 中企出海大会·游戏与互娱出海分论坛」,灵犀互娱基础架构负责人朱晓靖的演讲内容,从技术层面分享云原生架构护航灵犀互娱游戏出海经验。
|
IDE Java 数据库连接
IntelliJ IDEA安装使用教程——社区免费版——附中文插件安装
IntelliJ IDEA安装使用教程——社区免费版——附中文插件安装
7158 0
|
11月前
|
缓存 JavaScript UED
浏览器进程模型及事件循环机制
这篇文章介绍了在Vite工程化中如何手动分包以优化打包输出的过程。文章首先创建了一个Vue3工程,通过修改代码前后打包对比,指出每次打包都会导致包含第三方库的文件重新下载。接着,文章说明了如何将第三方依赖库和业务代码分别打包,通过配置`vite.config.ts`文件来实现手动分包,最终达到优化缓存效果,减少不必要的下载。
185 0
|
11月前
|
人工智能 量子技术 调度
别只盯着ChatGPT了,量子计算才是下一个能源“爆点”!
别只盯着ChatGPT了,量子计算才是下一个能源“爆点”!
395 17
|
11月前
|
人工智能 运维 Prometheus
运维再不“聪明点”,迟早被业务拖垮!
运维再不“聪明点”,迟早被业务拖垮!
402 0
|
6月前
|
运维 JavaScript Java
Java精品项目:基于SaaS的多租户ERP系统源码(含生产、财务、OA)
云ERP是面向小微企业的SaaS多租户管理系统,集成进销存、MRP生产、品质、仓储、财务、CRM、OA等一体化功能。基于SpringBoot+Vue架构,支持云端一键部署,具备高度集成、实时协同、灵活配置与安全可靠等核心优势,助力企业数字化转型。
488 0
|
弹性计算 运维 Linux
Docker环境如何配置?使用阿里云OOS一步搞定!
阿里云OOS简化了ECS上应用部署,提供Docker一键安装服务。支持多种操作系统,包括Alibaba Cloud Linux、CentOS、Ubuntu、Debian和Windows Server。要安装Docker,用户只需在OOS扩展程序中选择并确认实例,执行安装。OOS扩展程序还支持Java、Python等开发环境及宝塔面板、WordPress等应用的快速配置,提升运维效率,让云资源管理更便捷。
Docker环境如何配置?使用阿里云OOS一步搞定!
|
11月前
|
消息中间件 Web App开发 JavaScript
浏览器进程模型及事件循环机制
这篇文章介绍了浏览器进程模型及事件循环机制,包括进程和线程的基本概念,浏览器如何作为多进程多线程应用程序运行,以及渲染主线程如何通过消息队列来调度和执行各种任务,保证浏览器的稳定运行和任务的有序进行。
223 0
|
11月前
|
SQL 存储 JavaScript
如何开发人事管理系统中的招聘管理板块?(附架构图+流程图+代码参考)
本文介绍了企业人事管理系统中招聘管理模块的设计与开发,涵盖职位发布、简历筛选、面试跟进、Offer管理等全流程数字化内容。文章详细解析了模块架构、业务流程、核心开发技巧,并提供了示例代码和实现效果展示,同时解答了落地实施中的常见问题,助力企业高效构建招聘管理体系。