当前位置: 首页 > news >正文

一次渲染十万条数据:前端技术优化(上)

今天看了一篇文章,写的是一次性渲染十万条数据的方法,本文内容是对这篇文章的学习总结,以及知识点补充。

在现代Web应用中,前端经常需要处理大量的数据展示,例如用户评论、商品列表等。直接渲染大量数据会导致浏览器性能问题,如卡顿和延迟。本文将探讨几种优化策略,帮助开发者提高网页性能,优化用户体验。

方法一:通过document直接渲染十万条数据

示例代码

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>一次性渲染十万条数据</title>
</head>
<body><ul id="app"></ul>
</body>
<script>const app = document.querySelector('#app')let now = Date.now()// 方法一:一次性渲染十万条数据for (let i = 0; i < 100000; i++) {const li = document.createElement('li')li.innerText = `我是第${i + 1}条数据`app.appendChild(li)}console.log('js运行耗时', Date.now() - now)setTimeout(() => {console.log('dom渲染耗时', Date.now() - now)})</script>
</html>

结果

请添加图片描述

问题

当页面需要一次性渲染大量数据时,直接将所有数据渲染到DOM中会迅速消耗浏览器资源,造成性能瓶颈。这种方法虽然简单,但会导致浏览器响应缓慢,用户体验差。

补充知识1: JS事件循环之宏任务和微任务

在 JavaScript 中,事件循环是处理并发的机制,它允许执行非阻塞的操作。事件循环的核心概念包括宏任务(macro tasks)和微任务(micro tasks)。它们的执行顺序和机制十分重要,理解它们有助于更好地编写异步代码。

宏任务与微任务
  1. 宏任务 (Macro Task):

    • 宏任务是较大粒度的任务,它通常由以下几部分组成:
      • 整个脚本(执行的上下文)
      • setTimeout
      • setInterval
      • I/O 操作(如网络请求)
    • 宏任务的执行是按照创建顺序依次执行的。
  2. 微任务 (Micro Task):

    • 微任务是相对较小粒度的任务,通常用于处理短小的异步操作,主要由以下几部分组成:
      • Promise.then().catch()
      • MutationObserver
    • 微任务在当前宏任务执行完毕后立即执行,且在浏览器进行下一次重绘之前完成所有微任务。这意味着微任务的优先级高于宏任务。
执行顺序
  1. 首先,事件循环从宏任务队列中取出一个宏任务并执行。
  2. 执行完宏任务后,查看微任务队列,执行所有微任务,直到微任务队列为空。
  3. 一旦微任务队列为空,浏览器会进行渲染(重绘),然后再从宏任务队列中取出下一个宏任务。
  4. 重复这个过程,直到所有任务都完成。
示例

下面是一个简单的示例,帮助理解宏任务和微任务的执行顺序:

console.log('Start');setTimeout(() => {console.log('Timeout 1');
}, 0);new Promise((resolve, reject) => {console.log('Promise 1');resolve('Promise 1 resolved');
}).then((res) => {console.log(res);
});process.nextTick(() => {console.log('Next Tick');
});setTimeout(() => {console.log('Timeout 2');
}, 0);new Promise((resolve, reject) => {console.log('Promise 2');resolve('Promise 2 resolved');
}).then((res) => {console.log(res);
});console.log('End');
执行输出

执行上面的代码将会输出以下内容:

Start
Promise 1
Promise 2
End
Promise 1 resolved
Promise 2 resolved
Next Tick
Timeout 1
Timeout 2
  • 在 JavaScript 的事件循环中,宏任务的执行优先于微任务,但在宏任务完成后,微任务会立即执行,确保在下一个宏任务开始之前执行所有微任务。
  • 理解这一流程能够帮助开发者更好地预测和管理异步代码的执行顺序及其结果。

补充知识2: 执行渲染操作,更新界面为什么发生在执行 setTimeout之前?

在 JavaScript 中,事件循环和异步操作的处理方式是理解 setTimeout 和界面更新之间关系的关键。

界面更新的时机

在 JavaScript 中,界面更新通常在以下几个时刻发生:

  • 当主线程空闲时(即没有其他任务在执行时),浏览器会进行界面更新。
  • 在一个宏任务(如 setTimeout)执行之后,浏览器会检查微任务队列并执行所有的微任务,然后再进行界面更新。
为什么界面更新发生在执行 setTimeout 之前
  1. 执行顺序:当你调用 setTimeout 时,传入的回调函数不会立即执行,而是被放入宏任务队列中。主线程会继续执行当前的任务。
  2. 界面更新:在当前任务结束后,浏览器会检查是否有需要更新的界面。此时,如果有 DOM 的改变(例如通过某个函数修改了 DOM),浏览器会更新界面。
  3. 执行 setTimeout 的回调:在主线程空闲并完成微任务后,浏览器会从宏任务队列中取出 setTimeout 的回调并执行。
示例
console.log("Start");
setTimeout(() => {console.log("Inside setTimeout");
}, 0);
console.log("End");

输出顺序将是:

Start
End
Inside setTimeout

在 JavaScript 中,界面更新发生在当前任务完成后和 setTimeout 回调执行之前。理解这一点对于优化性能和确保用户界面流畅性非常重要。

补充知识3: 回流与重绘

页面的回流与重绘是指浏览器在渲染网页时的两种重要过程。

  1. 回流(Reflow):当页面的结构或内容发生变化时(比如增加、删除或修改元素的大小、位置等),浏览器需要重新计算元素的几何属性,以确定它们的位置和大小。这一过程称为回流。回流会影响整个文档的布局,因此较为耗性能。
  2. 重绘(Repaint):当元素的外观发生变化(比如背景色、字体颜色等)但并不影响布局时,浏览器只需重新绘制该元素,而无需重新计算其几何属性。这一过程称为重绘。重绘通常比回流消耗的性能要少。
    总结来说,回流是布局计算,重绘是外观更新。在优化网页性能时,应尽量减少这两个过程的发生。

方法二:分批渲染

为了解决直接渲染带来的性能问题,我们可以采用分批渲染的方法。通过将数据分成小块,逐一渲染,可以减轻浏览器的即时负担。

使用 setTimeout 进行分批渲染

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>一次性渲染十万条数据</title>
</head>
<body><ul id="app"></ul>
</body>
<script>const app = document.querySelector('#app')let now = Date.now()// 方法二:分批渲染十万条数据const total = 100000const loadOnce = 20const page = total / loadOnceconst index = 0function renderData(curTotal, curIndex) {let pageCount=Math.min(loadOnce,curTotal)setTimeout(() => {for (let i = 0; i < pageCount; i++) {const li = document.createElement('li')li.innerText = `我是第${i}条数据`app.appendChild(li)}renderData(curTotal-pageCount, curIndex + pageCount)})}renderData(total, index)</script>
</html>

结果

请添加图片描述

问题

当用户往下翻的时候有可能那一瞬间看不到东西

方法三、使用requestAnimationFrame替代setTimeout

requestAnimationFrame 是一种更高效的分批渲染方法,它允许在浏览器的绘制周期中执行动画和渲染,从而提高性能。

示例代码

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>一次性渲染十万条数据</title>
</head>
<body><ul id="app"></ul>
</body>
<script>const app = document.querySelector('#app')let now = Date.now()// 方法三:使用requestAnimationFrame渲染十万条数据const total = 100000const loadOnce = 20const page = total / loadOnceconst index = 0function renderData(curTotal, curIndex) {let pageCount=Math.min(loadOnce,curTotal)requestAnimationFrame(() => {for (let i = 0; i < pageCount; i++) {const li = document.createElement('li')li.innerText = `我是第${i}条数据`app.appendChild(li)}renderData(curTotal-pageCount, curIndex + pageCount)})}renderData(total, index)</script>
</html>

补充知识4: 什么是requestAnimationFrame?

requestAnimationFrame 是一个用于优化网页动画效果的 JavaScript 方法。它指示浏览器在下次重绘之前调用指定的回调函数,从而实现基于帧的动画效果。使用 requestAnimationFrame 的一个主要优点是它能够根据浏览器的绘制频率来调整动画的更新速率,从而使动画更加流畅和高效。

使用场景:
  1. 平滑动画:如果你在进行平移动画、旋转、缩放等效果时,使用 requestAnimationFrame 可以确保动画的每一帧都在浏览器的绘制周期内更新,这有助于避免由于使用 setTimeoutsetInterval 而导致的帧率不稳定。
  2. 减少 CPU 消耗requestAnimationFrame 还具有智能调节的功能,特别是在用户切换标签页或者浏览器窗口不在视野内时,它会自动停止调用回调函数,从而节省资源。

基本用法:

function animate() {// 更新动画状态// ...// 请求下一帧requestAnimationFrame(animate);
}
// 开始动画
requestAnimationFrame(animate);

在上面的代码中,animate 函数会执行动画的逻辑,并通过 requestAnimationFrame(animate) 请求下一帧的更新。这样,animate 函数会在浏览器准备好下一个绘制周期时被调用。

优势:

  • 帧率同步requestAnimationFrame 会使动画的帧率与浏览器的刷新率(通常是60帧每秒)同步,从而提高流畅性。

  • 性能优化:当页面不在视野中时,requestAnimationFrame 会暂停动画的执行,从而减少 CPU 和 GPU 的使用。

  • 简洁易用:API 简单直观,更容易使用来控制动画的生命周期。

总之,使用 requestAnimationFrame 是在现代网页应用中实现高性能动画的推荐方式。

方法四:利用 DocumentFragment

DocumentFragment 是一个轻量级的文档对象,可以用于在内存中组装一组节点,然后一次性添加到DOM中,减少DOM操作次数。

示例代码

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>一次性渲染十万条数据</title>
</head>
<body><ul id="app"></ul>
</body>
<script>const app = document.querySelector('#app')let now = Date.now()// 方法四:使用文档碎片渲染十万条数据const total = 100000const loadOnce = 20const page = total / loadOnceconst index = 0function renderData(curTotal, curIndex) {let documentFragment = document.createDocumentFragment()let pageCount=Math.min(loadOnce,curTotal)requestAnimationFrame(() => {for (let i = 0; i < pageCount; i++) {const li = document.createElement('li')li.innerText = `我是第${i}条数据`documentFragment.appendChild(li)}app.appendChild(documentFragment)renderData(curTotal-pageCount, curIndex + pageCount)})}renderData(total, index)</script>
</html>

总结

在处理大量数据渲染时,选择合适的方法至关重要。直接渲染虽然简单,但性能较差。分批渲染、requestAnimationFrameDocumentFragment 提供了更优的性能解决方案。开发者应根据具体情况选择最合适的方法,以确保应用的流畅性和用户体验。

补充知识5:什么是DocumentFragment?

DocumentFragment 是一个轻量级的文档对象,表示一个可以包含多个节点的虚拟容器。它不是文档中的实际部分,而是用来在内存中组装一组节点,然后一次性地将它们添加到文档中。这种做法可以提高性能,因为它减少了对 DOM 的多次操作,降低了重绘和重排的次数。

作用和优势:
  1. 性能优化:直接多次操作 DOM 会导致浏览器频繁地重绘和重排,从而降低性能。使用 DocumentFragment 可以将多个 DOM 操作合并为一个,从而显著提高性能。

  2. 内存管理DocumentFragment 只存在于内存中,直到你将它的内容添加到实际的 DOM 中。这使得在构建大型 DOM 结构时更节省内存。

  3. 灵活性:你可以使用 DocumentFragment 来临时组装和修改多个节点,然后一次性插入到 DOM 中,保持 DOM 的一致性。

基本用法:

下面是一个简单的例子,展示如何使用 DocumentFragment 来添加多个节点到 DOM 中:


// 创建一个 DocumentFragmentconst fragment = document.createDocumentFragment();// 创建几个新的元素const li1 = document.createElement('li');li1.textContent = 'Item 1';const li2 = document.createElement('li');li2.textContent = 'Item 2';const li3 = document.createElement('li');li3.textContent = 'Item 3';// 将元素添加到 DocumentFragment 中fragment.appendChild(li1);fragment.appendChild(li2);fragment.appendChild(li3);// 将 DocumentFragment 追加到现有的 DOM 中document.getElementById('myList').appendChild(fragment);

在上面的示例中,使用 DocumentFragment 创建并组合了多个 li 元素,最后一次性将它们添加到一个列表中。这样可以避免在将每个 li 添加到 DOM 时引起的多次重排和重绘。

DocumentFragment 是一个非常有用的工具,适用于需要频繁与 DOM 交互时,帮助保持性能和优化操作。在需要插入或修改多个节点时,使用 DocumentFragment 是个不错的选择。

参考文章:
https://juejin.cn/post/7407763018471948325

相关文章:

一次渲染十万条数据:前端技术优化(上)

今天看了一篇文章&#xff0c;写的是一次性渲染十万条数据的方法&#xff0c;本文内容是对这篇文章的学习总结&#xff0c;以及知识点补充。 在现代Web应用中&#xff0c;前端经常需要处理大量的数据展示&#xff0c;例如用户评论、商品列表等。直接渲染大量数据会导致浏览器性…...

springboot实训学习笔记(5)(用户登录接口的主逻辑)

接着上篇博客学习。上篇博客是已经基本完成用户模块的注册接口的开发以及注册时的参数合法性校验。具体往回看了解的链接如下。 springboot实训学习笔记&#xff08;4&#xff09;(Spring Validation参数校验框架、全局异常处理器)-CSDN博客文章浏览阅读576次&#xff0c;点赞7…...

python中网络爬虫框架

Python 中有许多强大的网络爬虫框架&#xff0c;它们帮助开发者轻松地抓取和处理网页数据。最常用的 Python 网络爬虫框架有以下几个&#xff1a; 1. Scrapy Scrapy 是 Python 中最受欢迎的网络爬虫框架之一&#xff0c;专为大规模网络爬取和数据提取任务而设计。它功能强大、…...

GEC6818初次连接使用

目录 1.开发板资源接口​编辑​编辑 2.安装 SecureCRT工具 2.1SecureCRT相关问题 3.连接开发板 4.开发板文件传输 4.1串口传输 rx 从电脑下载文件到开发板 sz 从开发板把文件发送到电脑 4.2U盘/SD卡传输 4.3网络传输[重点] 5.运行传到开发板的可执行文件 6.开发板网络…...

解释下不同Gan模型之间的异同点

生成对抗网络&#xff08;GAN, Generative Adversarial Network&#xff09;是一类强大的生成模型。随着时间的推移&#xff0c;研究人员提出了许多不同的 GAN 变体来改善原始模型的性能或针对特定任务进行优化。下面将解释一些常见的 GAN 变体&#xff0c;并讨论它们的异同点。…...

Hadoop的一些高频面试题 --- hdfs、mapreduce以及yarn的面试题

文章目录 一、HDFS1、Hadoop的三大组成部分2、本地模式和伪分布模式的区别是什么3、什么是HDFS4、如何单独启动namenode5、hdfs的写入流程6、hdfs的读取流程7、hdfs为什么不能存储小文件8、secondaryNameNode的运行原理9、hadoop集群启动后离开安全模式的条件10、hdfs集群的开机…...

Day99 代码随想录打卡|动态规划篇--- 01背包问题

题目&#xff08;卡玛网T46&#xff09;&#xff1a; 小明是一位科学家&#xff0c;他需要参加一场重要的国际科学大会&#xff0c;以展示自己的最新研究成果。他需要带一些研究材料&#xff0c;但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等&am…...

往证是什么意思

“往证”通常是在数学证明中使用的一种方法&#xff0c;尤其是在证明某个结论的相反&#xff08;即否定&#xff09;是错误的情况下。具体来说&#xff0c;就是假设结论不成立&#xff0c;然后通过逻辑推理展示出这种假设导致矛盾&#xff0c;从而得出原结论必然成立。 举例说…...

Camunda流程引擎并发性能优化

文章目录 Camunda流程引擎一、JobExecutor1、工作流程2、主要作用 二、性能问题1、实际场景&#xff1a;2、性能问题描述3、总结 三、优化方案方案一&#xff1a;修改 Camunda JobExecutor 源码以实现租户 ID 隔离方案二&#xff1a;使用 max-jobs-per-acquisition 参数控制上锁…...

spring springboot 日志框架

一、常见的日志框架 JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j.... 注意&#xff1a;SLF4j 类似于接口 Log4j &#xff0c;Logback 都是出自同一作者之手 JUL 为apache 公司产品 Spring&#xff08;commons-logging&#xff09;、Hibernate&#xff08;jboss…...

【D3.js in Action 3 精译_022】3.2 使用 D3 完成数据准备工作

当前内容所在位置 第一部分 D3.js 基础知识 第一章 D3.js 简介&#xff08;已完结&#xff09; 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践&#xff08;上&#xff09;1.3 数据可视化最佳实践&#xff08;下&#xff09;1.4 本章小结 第二章…...

电脑怎么禁用软件?5个方法速成,小白必入!

电脑禁用软件的方法多种多样&#xff0c;以下是五种简单易行的方法. 适合不同需求的用户&#xff0c;特别是电脑小白。 1. 使用任务管理器禁用启动项 操作步骤&#xff1a;按下“Ctrl Shift Esc”组合键&#xff0c;打开任务管理器。 切换到“启动”选项卡&#xff0c;找到…...

力扣之181.超过经理收入的员工

文章目录 1. 181.超过经理收入的员工1.1 题干1.2 准备数据1.3 题解1.4 结果截图 1. 181.超过经理收入的员工 1.1 题干 表&#xff1a;Employee -------------------- | Column Name | Type | -------------------- | id | int | | name | varchar | | salary | int | | mana…...

C++语法应用:从return机制看返回指针,返回引用

前言 编程是极其注重实践的工作,学习的同时要伴随代码 引入 此前对返回指针和引用有一些纠结&#xff0c;从return角度来观察发生了什么。 return机制 函数中return表示代码结束&#xff0c;如果return后面有其他代码将不被执行。 return发生了值转移&#xff0c;return后面的…...

Linux5-echo,>,tail

1.echo命令 echo是输出命令&#xff0c;类似printf 例如&#xff1a;echo "hello world"&#xff0c;输出hello world echo pwd&#xff0c;输出pwd的位置。是键盘上~ 2.重定向符> >> >指把左边内容覆盖到右边 echo hello world>test.txt >…...

sqlgun靶场训练

1.看到php&#xff1f;id &#xff0c;然后刚好有个框&#xff0c;直接测试sql注入 2.发现输入1 union select 1,2,3#的时候在2处有回显 3.查看表名 -1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schemadatabase()# 4.查看列名…...

简化登录流程,助力应用建立用户体系

随着智能手机和移动应用的普及&#xff0c;用户需要在不同的应用中注册和登录账号&#xff0c;传统的账号注册和登录流程需要用户输入用户名和密码&#xff0c;这不仅繁琐而且容易造成用户流失。 华为账号服务(Account Kit)提供简单、快速、安全的登录功能&#xff0c;让用户快…...

【研发日记】嵌入式处理器技能解锁(六)——ARM的Cortex-M4内核

文章目录 前言 背景介绍 指令集架构 ARM起源 ARM分类 Cortex-M4 内核框架 指令流水线 实践应用 总结 参考资料 前言 见《【研发日记】嵌入式处理器技能解锁(一)——多任务异步执行调度的三种方法》 见《【研发日记】嵌入式处理器技能解锁(二)——TI C2000 DSP的SCI(…...

深度学习经典模型之T5

T5(Text-to-Text Transfer Transformer) 是继BERT之后Google的又外力作&#xff0c;它是一个文本到文本迁移的基于Transformer的NLP模型&#xff0c;通过将 所有任务统一视为一个输入文本并输出到文本(Text-to-Text)中&#xff0c;即将任务嵌入在输入文本中&#xff0c;用文本的…...

10.第二阶段x86游戏实战2-反编译自己的程序加深堆栈的理解

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 工具下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…...

ARM总复习

1.计算机的组成 输入设备 输出设备 存储设备 运算器 控制器、总线 2.指令和指令集 2.1 机器指令 机器指令又叫机器码&#xff0c;在运算器内部存在各种运算电路&#xff0c;当处理器从内存中获取一条机器指令&#xff0c;就可以按照指令让运算器内部的指定的运算电路进行运…...

​​使用ENVI之大气校正(下)

再根据遥感影像的拍摄时间将Flight ate与Flight Time GMT (H:M:SS)填写&#xff0c;如要查询按如下方法 这里按照表中的内容修改 根据影像范围的经纬度与拍摄时间更改Atmospheric Model&#xff0c;更改完成后点击Multispectral Settings...在跳出的界面中选择GUI再点击Default…...

C++(学习)2024.9.18

目录 C基础介绍 C特点 面向对象的三大特征 面向对象与面向过程的区别 C拓展的非面向对象的功能 引用 引用的性质 引用的参数 指针和引用的区别 赋值 键盘输入 string字符串类 遍历方式 字符串与数字转换 函数 内联函数 函数重载overload 哑元函数 面向对象基…...

认知小文2《成功之路:习惯、学习与实践》

内容摘要&#xff1a; 在这个充满机遇的时代&#xff0c;成功不再是偶然&#xff0c;而是可以通过培养良好习惯、持续学习和实践来实现的目标。    一、肌肉记忆&#xff1a;技能的基石 成功往往需要像运动员一样&#xff0c;通过日复一日的练习来形成肌肉记忆。无论是健身…...

【数据仓库】数据仓库层次化设计

一、基本概念 **1. RDS&#xff08;RAW DATA STORES&#xff0c;原始数据存储&#xff09;** RDS作为原始数据存储层&#xff0c;用于存储来自各种源头的未经处理的数据。这些数据可能来自企业内部的业务系统、外部数据源或各种传感器等。RDS确保原始数据的完整性和可访问性&…...

【DAY20240918】03教你轻松配置 Git 远程仓库并高效推送代码!

文章目录 前言 git diff一、远程仓库&#xff1f;1、在 Gitee 上新建仓库&#xff1a;2、Git 全局设置&#xff1a;3、添加远程仓库&#xff1a;4、推送本地代码到远程仓库&#xff1a;5、输入用户名和密码&#xff1a;6、后续推送&#xff1a; 二、全情回顾三、参考 前言 git …...

从IPC摄像机读取视频帧解码并转化为YUV数据到转化为Bitmap

前言 本文主要介绍根据IPC的RTSP视频流地址,连接摄像机,并持续读取相机视频流,进一步进行播放实时画面,或者处理视频帧,将每一帧数据转化为安卓相机同格式数据,并保存为bitmap。 示例 val rtspClientListener = object: RtspClient.RtspClientListener {override fun …...

LeetCode 面试经典 150 题回顾

目录 一、数组 / 字符串 1.合并两个有序数组 &#xff08;简单&#xff09; 2.移除元素 &#xff08;简单&#xff09; 3.删除有序数组中的重复项 &#xff08;简单&#xff09; 4.删除有序数组中的重复项 II&#xff08;中等&#xff09; 5.多数元素&#xff08;简单&am…...

【网络安全的神秘世界】渗透测试基础

&#x1f31d;博客主页&#xff1a;泥菩萨 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 | 每天学会一个渗透测试工具 渗透测试基础 基于功能去进行漏洞挖掘 1、编辑器漏洞 1.1 编辑器漏洞介绍 一般企业搭建网站可能采用了通用模板&#xff…...

【重学 MySQL】二十九、函数的理解

【重学 MySQL】二十九、函数的理解 什么是函数不同 DBMS 函数的差异函数名称和参数功能实现数据类型支持性能和优化兼容性和可移植性 MySQL 的内置函数及分类单行函数多行函数&#xff08;聚合函数&#xff09;使用注意事项 什么是函数 函数&#xff08;Function&#xff09;在…...