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

前言

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

场景复现

我们看一段代码:

<!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. 每一次分片执行多少

这里我们就要用到一个 API :requestIdleCallback

这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间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. 分片高阶函数的封装

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/640911.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微软如何打造数字零售力航母系列科普02 --- 微软低代码应用平台加速企业创新 - 解放企业数字零售力

微软低代码应用平台推动企业创新- 解放企业数字零售力 微软在2023年GARTNER发布的魔力象限图中处于头部领先&#xff08;leader&#xff09;地位。 其LCAP产品是Microsoft Power Apps&#xff0c;扩展了AI Builder、Dataverse、Power Automate和Power Pages&#xff0c;这些都包…

[BT]BUUCTF刷题第20天(4.22)

第20天 Web [GWCTF 2019]我有一个数据库 打开网站发现乱码信息&#xff08;查看其他题解发现显示的是&#xff1a;我有一个数据库&#xff0c;但里面什么也没有~ 不信你找&#xff09; 但也不是明显信息&#xff0c;通过dirsearch扫描得到robots.txt&#xff0c;然后在里面得…

keil把c语言函数转成汇编

汇编可以让开发人员从根源上理解程序的运行逻辑&#xff0c;本文介绍如何在keil环境下如何把一个c文件中的某一个函数&#xff0c;转换为汇编函数&#xff0c;并编译运行。 右击某个c文件&#xff0c;选择Option for File。。。 图1 然后把下图中的Generate Assembler SRC Fi…

自动驾驶控制算法

本文内容来源是B站——忠厚老实的老王&#xff0c;侵删。 三个坐标系和一些有关的物理量 使用 frenet坐标系可以实现将车辆纵向控制和横向控制解耦&#xff0c;将其分开控制。使用右手系来进行学习。 一些有关物理量的基本概念&#xff1a; 运动学方程 建立微分方程 主要是弄…

【bug】使用mmsegmentaion遇到的问题

利用mmsegmentaion跑自定义数据集时的bug处理&#xff08;使用bisenetV2&#xff09; 1. ValueError: val_dataloader, val_cfg, and val_evaluator should be either all None or not None, but got val_dataloader{batch_size: 1, num_workers: 4}, val_cfg{type: ValLoop}, …

2024免费MAC苹果电脑系统优化软件CleanMyMac X

CleanMyMac X确实是一款专为Mac用户设计的清理和优化工具。它提供了一系列功能&#xff0c;旨在帮助用户释放磁盘空间、提升Mac的性能&#xff0c;并保护用户的隐私。 CleanMyMac X能够智能地扫描和识别Mac上的各种垃圾文件&#xff0c;如系统缓存、日志文件、无用的语言包等&…

【React】Day6

项目搭建 基于CRA创建项目 CRA是一个底层基于webpack快速创建React项目的脚手架工具 # 使用npx创建项目 npx create-react-app react-jike# 进入到项 cd react-jike# 启动项目 npm start调整项目目录结构 -src-apis 项目接口函数-assets 项目资源文件&…

Rancher-Longhorn-新增磁盘以及卷创建原理和卷副本调度规则

一、添加磁盘-官网指引 重点在于&#xff1a; 1、比如你新增了一块盘&#xff0c;你需要做一下事情&#xff1a; 1、执行 lsblk 能找到你的盘。 2、然后执行 fdisk /dev/sdxx 分区你的盘。 3、然后对于分区部署文件系统&#xff0c; mkfs.xfs 4、然后执行 mount /dev/sdxxx 你…

从数据库中到处所有表的列、注释、类型、是否必填等信息

从数据库中到处所有中文表名、英文表名、所有列、注释、类型、长度、是否必填等信息&#xff0c;效果如下&#xff1a; 要实现上面的表格可以直接用SQL实现&#xff0c;实现SQL如下&#xff1a; #查询SQL select* FROMinformation_schema.COLUMNS as columns left join (sele…

华为机考入门python3--(17)牛客17- 坐标移动

分类&#xff1a;字符串 知识点&#xff1a; 正则匹配 re.match(pattern, move) 格式字符串&#xff0c;可以在字符串中直接引用变量 f"{x},{y}" 题目来自【牛客】 import re def is_valid_coordinate(move): # 使用正则表达式验证移动是否合法 # ^: …

并发编程之线程通信及Condition的原理分析

1. synchronized中的线程通信 调用wait方法会使线程处于等待状态&#xff0c;直到另一个线程调用notify线程才会唤醒等待中的某个线程&#xff0c;生产者和消费者模型可以很好的使用到该例子。 生产者代码: public class Producer implements Runnable {private Queue<Str…

CountDownLatch倒计时器源码解读与使用

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正 目录 1. 前言 2. CountDownLatch有什么用 3. CountDownLatch底层原理 3.1. count…